Skip to content

Windows desktop app: browse, extensions, mpv playback, and downloads - #3262

Closed
weaponlions wants to merge 120 commits into
recloudstream:masterfrom
weaponlions:master
Closed

weaponlions wants to merge 120 commits into
recloudstream:masterfrom
weaponlions:master

Conversation

@weaponlions

Copy link
Copy Markdown

Summary

This adds a working Windows desktop version of CloudStream, built on the existing Compose Multiplatform desktopApp / shared modules. It runs the same extensions as Android: it downloads them from the usual repositories, converts them for the JVM, and runs them. Video plays through libmpv.

63 commits; 190 files changed (+19,016 / −152). Almost all of it is new desktop-only code. Changes to shared code are small and listed under "Changes outside the desktop app".

Features

Browsing

  • Home with a featured banner, Search with history, title pages with seasons and episodes.
  • Library and Continue watching, which resumes from the last position and the last source used.

Extensions

  • Add repositories by URL, short code or MegaRepo, then install, update and remove extensions.
  • Android extensions are converted from dex to JVM bytecode. Android APIs that desktop doesn't have are stubbed, and the Installed page says when part of an extension couldn't be converted.
  • Extension code runs with limits on time and memory, so one bad extension can't take down the app.

Player (libmpv embedded in the window)

  • Seek bar with a buffering indicator, buttons to skip ±10 s, volume up to 130 %, and a brightness control.
  • Pick the audio track, subtitles, playback speed and aspect ratio / zoom.
  • Subtitle styling and automatic language selection, as on Android.
  • Keyboard shortcuts, full screen, and the mouse wheel sets volume on the right half and brightness on the left, as on Android.
  • When a source fails, the player moves on to the next one and carries on from the same position.
  • A Skip button plays the best link found so far while extensions are still searching.
  • A far seek that the server never answers is retried on a new connection.

Network

  • Cloudflare checks and WebView lookups are solved with a headless Microsoft Edge. A browser window opens only when a site needs a human check and the user asks for it.
  • One shared HTTP client for the whole app.

Downloads and torrents

  • Download movies and episodes to watch offline, choosing the source as on Android.
  • Torrents and magnet links stream through TorrServer, which is installed only after the user agrees.

Settings and appearance

  • Settings are saved, the window remembers its size and position, and logs go to %APPDATA%\CloudStream\logs.
  • Dark and light themes, with buttons, chips and dialogs styled after the Android app.

Changes outside the desktop app

  • settings.gradle.kts: adds the :androidCompat module.
  • androidCompat/ (new): JVM stand-ins for the Android classes that extensions use (Context, SharedPreferences, Activity, Log, …), plus the dex converter and stub generator.
  • library/:
    • New DesktopWebView interface. WebViewResolver.jvm.kt uses it when the desktop app installs one; otherwise behaviour is unchanged.
    • MainAPI.kt: a bug fix in Episode.addDate. A date with a time but no time-zone offset was read as UTC; it now uses the device's time zone, as the code comment already said it should. This affects Android too.
  • shared/: view models (home, search, detail, links), stores for the library and settings, icons and strings, and small theme changes.
  • gradle/libs.versions.toml: new desktop dependencies.

Build and run

.\gradlew.bat :desktopApp:run                    # run from source
.\gradlew.bat :desktopApp:packageMsi             # build the Windows installer
.\gradlew.bat :shared:jvmTest                    # desktop unit tests

libmpv is built from source by .github/workflows/libmpv.yml (Clang, same setup as shinchiro's builds). The complete source of every bundled library is published with the release, to meet the LGPL.

CI and release

  • desktop.yml: builds the MSI and runs the JVM tests.
  • release.yml: publishes a beta of the Windows app on every merge into master. The app has a built-in updater that checks those releases.
  • libmpv.yml: builds libmpv and its source archive.

To decide before merging

  • Fork-specific URLs: the libmpv and 7zr downloads (desktopApp/build.gradle.kts) and the updater (AppUpdater.REPOSITORY) still point to harsh-pockt/cloudstream releases. They need to point to this repo's releases, which means running libmpv.yml here once.
  • Release cadence: whether you want a beta published on every merge, or releases only on demand.
  • Platforms: Windows only for now. The mpv embedding and Edge integration use Win32 APIs; macOS and Linux would need their own versions.

Known limitations

  • Extensions that depend heavily on Android UI, or on parts that can't be converted, may only partly work. The app says so on the Installed page.
  • Links protected by DRM can't play on desktop; they are skipped, and the player says how many were.

Testing

  • Unit tests in shared/src/jvmTest cover plugin loading and conversion, downloads, the watch store, subtitle settings, preferences, the HTTP client, the system browser and TorrServer.
  • The app has been used day to day on Windows 11, with a range of extensions from MegaRepo.

harsh-pockt and others added 30 commits October 1, 2026 23:19
- Fix the icon path (icons live in src/desktop-icons)
- Take packageVersion from versionName instead of a hardcoded 1.0.0
- Set a fixed upgradeUuid so new versions upgrade in place
- Add Start menu entry, desktop shortcut and install folder picker
- Bundle the JDK modules the app needs (java.net.http etc.)
- New desktop.yml workflow: runs library jvmTest and packageMsi on
  windows-latest, uploads the MSI

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: Windows MSI installer and CI job
- JvmPreferenceStore keeps every preference in one JSON file under
  %APPDATA%\CloudStream (or ~/.config, ~/Library/Application Support),
  written atomically; a corrupt file falls back to defaults and is kept
  as settings.json.corrupt
- AppDirs resolves the data folder, overridable with -Dcloudstream.home
- DesktopLog copies stdout/stderr into logs/cloudstream.log with
  timestamps and keeps the previous run as cloudstream.old.log
- SavedWindowState restores window size, position and maximized state,
  ignoring positions on monitors that are no longer connected
- Unit tests for the preference store, run in the desktop CI job

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: save settings, log to a file, remember the window
- AppShell with a navigation rail (Home, Search, Library, Settings);
  Ctrl+1..4 switch destination, Escape / Alt+Left go back
- Desktop settings screens (General, Player, Layout, Providers) built on
  the shared Mihon preference widgets and AppSettings keys, leaving out
  Android-only options (TV, gestures, battery, rotation, PiP, APK)
- Theme and primary color follow the settings live
- English strings and vector icons copied from app/res into the shared
  compose resources, keeping the Android names

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: navigation rail and settings screens
- PluginLoader loads the cross-platform .jar that extensions publish with
  isCrossPlatform = true; the jar has no manifest.json, so the plugin
  class is found by its @CloudstreamPlugin annotation. Unloading removes
  the plugin's providers and extractors and closes the class loader so
  Windows can replace or delete the jar
- RepositoryClient reads repo.json and plugin lists (including jarUrl,
  jarHash, jarFileSize) and verifies size and SHA-256 of downloads. It
  uses OkHttp because the JDK HTTP clients only try the first resolved
  address, and one GitHub address is unreachable on some ISPs
- DesktopPluginManager installs into <app data>/plugins, keeps
  installed.json, loads everything at startup in the background and
  records per-plugin load errors
- Settings > Extensions: add/remove repositories (one click for the
  official one), install/update/uninstall, Android-only plugins listed
  as such
- Tests: annotation scan, load/unload, list parsing, install/restart/
  uninstall and hash rejection against a local HTTP server, plus an
  opt-in online test (CLOUDSTREAM_ONLINE_TESTS=1) that installs a real
  plugin from the official repository and searches with it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: install and load extensions
Shared MVI view models for Search and Home in shared/commonMain, following
COMPOSE.md, with the desktop screens built on them.

- Search queries every enabled provider at once and fills each provider's
  row as it answers. Failures are listed separately. Rows with more pages
  get a More button. Ctrl+F opens Search.
- Home shows one provider's home page, picked from a menu. The choice is
  saved under home_api_used and falls back to the first provider when that
  one is removed. Home refreshes when plugins finish loading or the
  provider settings change.
- Providers are filtered by the same language and media-type settings as
  the Android app, and hidden search qualities are dropped. Provider calls
  use the same timeouts as APIRepository.
- Posters load through Coil with OkHttp and a disk cache in LOCALAPPDATA,
  sending the provider's poster headers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Clicking a poster opens its details. Play finds the links and plays the
best one in the app window with libmpv.

- Player: libmpv over JNA, embedded in the window. libVLC was tried first
  but can only send a Referer and User-Agent, while many extensions need
  other headers too. mpv sends every header the extension gives on every
  request, playlists and segments included. Plays MP4, HLS and DASH with
  hardware decoding. mpv's own controls handle seeking.
- libmpv-2.dll comes from a pinned shinchiro build, downloaded at build
  time, checked against its SHA-256 and installed with the app together
  with a third-party notice and the GPL text.
- Shared DetailViewModel: movie, series and anime pages, seasons grouped
  with their names, sub/dub choice, torrents marked unsupported.
- Shared LinksViewModel: links stream in, best quality first. The best link
  plays once all are in, or 6 s after the first if extractors are slow. A
  link that fails to play moves on to the next. Torrent and DRM links are
  skipped and counted.
- Player screen: a bar with Source, Subtitles and Audio pickers that open
  beside the video, since nothing can be drawn over the native video
  window. Extension subtitles are downloaded with their own headers.
  Space, arrows, F and Escape work, inside the video too.
- Navigation: details and player open as pages on top of Home or Search,
  each owning its view models. Full screen hides the rail; Escape leaves
  full screen, then the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: detail page and video playback
The Add repository box takes what Android takes: a URL, a cloudstreamrepo://
or https://cs.repo/ link, or a short code such as "megarepo", looked up at
cutt.ly (or py.md for "!code") like RepositoryManager.parseRepoUrl.

MegaRepo holds one plugin, MegaProvider, an Android helper that adds every
repository in recloudstream/cs-repos through app internals desktop does not
have. So when a short code or URL leads to MegaRepo, the app adds those
community repositories itself. Opening MegaRepo explains this and offers
the same, and the Extensions screen has an "Add all community
repositories" button.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: repository short codes and MegaRepo
- Added DexConverter for converting Android extensions (.cs3) to JAR files for desktop compatibility.
- Introduced MissingMembers to handle missing methods in desktop versions of Android classes.
- Created StubGenerator and StubModel to generate stubs for Android classes with missing members.
- Established StubHierarchy to define the superclass relationships for stubbed Android classes.
- Developed AndroidExtensionsTest to validate the functionality of Android extensions on desktop, including conversion and search capabilities.
- Extensions is now a menu of three pages: Repositories, Installed
  extensions and Add repository.
- Repositories are listed by the name in their repo.json, kept so the
  list shows them right away, or "Repo (owner)" until it is known.
- Installed extensions checks their repositories for new versions, with
  Update and Update all, and updates them at startup unless turned off.
- Settings has Updates: the app finds newer releases on GitHub, checks
  the installer's SHA-256 and installs it with msiexec. It also asks at
  startup unless turned off.
- Settings has Storage: clear the cache, remove all extensions or erase
  everything, done on a restart because loaded extensions keep their
  jars locked.
- After CloudStream is installed again, it asks whether to keep the old
  data or start fresh. Updates made through the app do not ask.
- Stubs of the app's sync providers extend SyncAPI as in the app, which
  CineStream needs to load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: extension pages, updates and fresh start
- Library tab: titles in Watching, Plan to watch, On hold, Completed or
  Dropped, set from Add to library on a title's page.
- The player saves how far the user got and starts there next time; a
  new source carries on from the same point.
- Home shows Continue watching, which moves on to the next episode
  once one is nearly finished, like the Android app.
- Title pages show Resume with the time, and each episode's progress
  or that it was watched.
- Saved in the Android app's DataStoreHelper keys and JSON, with its
  title and episode ids, so a later backup import works either way.
- Remove all extensions keeps the app's data store, which now holds
  the library.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: library and resume watching
Recent searches on the Search page, stored in the Android app's
search_history keys. Extension toasts (android.widget.Toast and
CommonActivity.showToast) show as a message in the app window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: search history and extension toasts
- A control bar under the video: play/pause, -10/+10 s, a seek bar with
  the time, mute, volume and full screen. In full screen the bars show
  while the mouse moves and hide after 3 s. mpv's own controller is off:
  embedded, mpv gets no mouse input, so it never worked.
- The mouse over the video is handled on the canvas: a click closes an
  open panel or pauses, a double click toggles full screen, the wheel
  changes the volume. Escape closes an open panel first.
- Source names show in full, in a wider panel.
- Subtitles: the picked subtitle is added again when the source changes,
  extension subtitles are matched to mpv tracks by file, sorted by
  language, numbered when a language repeats and show their host. A
  Timing control moves them earlier or later. New Speed panel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: easier player controls, panels and subtitles
- Loading and buffering show as a spinner with text drawn by mpv over
  the video: finding links, opening the file, seeking, or the cache
  running empty ("Buffering 45%").
- Dark player bars in every theme. A thin seek bar across the top of
  the controls shows what is played and what is loaded, thickens under
  the mouse and shows the time under it. Icon buttons for 10 s back and
  forward, mute and volume; Subtitles, Audio (with the track count),
  Speed and Source (with the quality) on the right.
- The Audio button is always there. Tracks show their language in
  full, channels, codec and the default; with one track the panel says
  other sources may have other languages.
- A link that ends within seconds of starting counts as failed and the
  next one plays, and progress is only saved once a link really plays,
  so a broken file no longer marks an episode watched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: player redesign, buffering spinner and audio picker
- The player's keys go through the window before anything else, so
  they work whatever has the focus. In full screen the bars hide with
  the button that had the focus, which left Space and the arrows doing
  nothing. The window's own shortcuts ran first too, so Escape with a
  panel open closed the player instead of the panel.
- Escape closes an open panel, then leaves full screen, then goes back.
  K, J and L work like Space and the arrows. Keys that change something
  show the bars in full screen.
- Real full screen on Windows: the window drops its title bar and
  border and covers the monitor, then gets them back. Compose's full
  screen kept the title bar. A maximized window is restored first and
  maximized again after, so it does not come back under the taskbar.
- The mouse's back button goes back everywhere, the video included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
M pauses the player and hides the whole window, gone from the taskbar and
Alt+Tab. A tray icon, shown only while hidden, brings it back as it was.
Typing an M into a text field still types it: the text fields' input
sessions are tracked, since their letters don't come as key events.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: predictable player keys, full screen and back
Desktop: M hides the app and pauses the video
harsh-pockt and others added 26 commits October 16, 2026 21:36
…from servers that cannot seek

mpv starts a file at the position it is given, but only when it can jump
into the file as it opens. A server that ignores range requests does not
let it, and the new source started from the beginning. Once the file
plays, the player checks where it started and seeks there if it was
more than 5 s short. Each resume is logged, with whether the stream was
seekable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop player: switching source resumes, also from servers that cannot seek
…ing that scrolls

- The title page has a Search button: it opens Search with the title
  typed in and searched, to find it in the other extensions
- Continue watching is part of the home page's list, so it scrolls away
  with the rows instead of staying at the top
- Its cards are small, 84 dp high: a thumbnail, the title, the episode
  and the minutes left, the progress, and an x to drop it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, bigger headings

- Posters and Continue watching cards grow 6 % under the mouse, over
  their neighbours, with a shadow
- Rows show arrows at their ends while the mouse is over them, which
  scroll by most of the row's width: a mouse cannot swipe
- The default dark theme is neutral and darker, after Netflix: the
  background #141414, surfaces a step lighter, gray text #B3B3B3. The
  other themes keep the Android app's gray text
- Row titles are bold and larger, and the rows have 32 dp margins

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Home opens on the provider's first title, large: its backdrop faded
  into the page, its name, year, length, genres and the start of its
  story, and More info. The rows only have posters, so the rest loads
  from its page, the poster standing in until then
- The side bar is 72 dp, in the page's colour, icons only: the current
  page is lit and marked with a bar, and each icon names its page on
  hover

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sky-blue accent

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop: Search button on title pages, smaller scrolling Continue watching
Desktop UI: hover zoom, row arrows, darker theme, bigger headings
Desktop UI: featured banner on Home, slim side bar
Desktop UI: dark theme in Prime Video colours
…g plays at once from the last source

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd age rating

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop UI: floating glass dock, Continue watching plays directly
Desktop UI: Home banner uses the extension's logo, score and age rating
…s header

The window's title bar and border are painted in the theme's colours through DWM
(exact colours on Windows 11, dark title bar on Windows 10), again whenever the
theme changes. The player's bars and side panels use a dark theme's own colours,
neutral greys under a light theme. The settings header takes the page's colour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop UI: the theme on the window frame, the player and Settings
…ar and compact downloads

- Light theme in the Prime colours (white, cool blue-gray, deeper sky blue); "Dark" is now
  the Prime navy and the default, the pure black one is named "Black"; the Normal accent
  swatch shows the theme's real accent
- Settings: a readable centred width, each group as rounded cards in the theme's box
  colour, flat switches and a slider without step dots, the header the page's colour
  also when scrolled, the info line with its icon beside the text
- Home: the provider and Reload in a floating glass pill like the dock, the provider list
  a rounded themed menu with the current one ticked and a filter box past 8 providers
- Downloads: one compact card per title, a movie on a single line
- Title page: the backdrop behind the header, a shadowed poster, the score, rounded
  episode lines; Search's box rounded; the glass shared by the dock and the Home bar,
  with a dark edge on light themes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop UI: Prime-style light theme, settings cards, glass Home bar, compact downloads
…a clean hover on light themes

- Posters cast their hover shadow from the picture alone, so the title under them no longer
  sits in a grey box on a light theme; the row arrows are round buttons over a soft fade
- Buttons are pills: the main action in the theme's accent, the others in its tonal colour,
  dialogs too
- Title page: the backdrop across most of the window with the poster, logo or name, facts
  with the score in the accent, genre pills and a story that expands; Play (or Play S1 E1
  for a series not started), Start over, the library, download, and round tools for other
  sources, the trailer, the provider's site and copying the link; how far it was watched;
  episodes as a grid of cards with progress and a play sign on hover; the cast; More like
  this; and an About card with everything known about the title

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop UI: fuller title page, accent pill buttons, clean hover on light themes
A file Windows would not delete for a moment (the player or a virus scan holding it) was
left on the disk for good, its record already gone. It is tried again for a few seconds,
then deleted when the app closes. Folders a deleted download leaves empty, such as a
series' folder after its last episode, go too; the downloads folder stays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Downloads: deleting a download cleans up safely, even a file held open
- Desktop Build checks pull requests only, not the merge again; a new push cancels the check
  of the commit before it; the installer is built only when run by hand; time limit 40 min
- Release runs only when asked (Run workflow on master, or a v tag), no longer on every merge,
  and links to the libmpv release for the dll's source instead of uploading the same 462 MB
  with every release
- libmpv (about 70 min a build) runs only by hand, not on pull requests that touch it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI: spend far fewer GitHub Actions minutes
…le again there

A seek past what is loaded asks the server for that part of the file. Some
file hosts never answer that request, and mpv then waits at the new spot for
good, the spinner turning: mpv's network-timeout does not end a stalled
transfer. After 12 s of waiting the file is opened again at that point, on a
new connection, with the chosen audio track and subtitle, at most twice per
seek. A slow host that does answer (8 s in tests) is left alone.

mpv's errors now go to the app's log, so a refusing server shows there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Desktop player: a far seek that the server never answers opens the fi…
@fire-light42

Copy link
Copy Markdown
Collaborator

You have created a very cool project, but it is not something I can merge.

Claude is very good at creating demos, or something which works on your own machine. It is seemingly not good at creating maintainable or portable code. This is visible in two core issues of this code: Inherent Windows dependency and lots of duplicated code. The entire AndroidCompat will require duplicated effort when changing anything and invariably cause weird issues. The heavy Windows dependency removes all upsides of working with Compose. We are aiming to make CloudStream platform neutral using KMP and target many different platforms.

We are working on making CloudStream Windows-compatible, but taking shortcuts (such as Windows-only, or duplicated code) in the official project will slow down the project too much in the long run.

@weaponlions

Copy link
Copy Markdown
Author

Thanks for taking the time to review my PR and explain your concerns. I understand your points regarding code duplication, Windows-specific dependencies, and the importance of keeping CloudStream platform-neutral and maintainable.

I’m still very interested in contributing to CloudStream, and I’d really appreciate an opportunity to work on the desktop version. I’m happy to start with any bugs, smaller features, or improvements that align with the project’s architecture and KMP goals.

I’m also willing to rework my approach and learn from the project’s existing patterns to ensure my contributions are maintainable and portable.

I understand if this PR isn’t suitable for merging in its current state, but I’d be grateful for a chance to contribute in a way that genuinely benefits the project. If there’s an issue you think would be a good starting point, I’d love to take it on.

@fire-light42

Copy link
Copy Markdown
Collaborator

That's great to hear!

CloudStream would really benefit from more help! Currently the desktop compose migration requires a lot of precision to get right, in an admittedly confusing code base. This makes it difficult for new contributors to help. However, this will only get easier in time as more of the difficult stuff is solved.

I would suggest looking into how smaller bugs or features can be implemented within the existing compose rework (currently only Settings). This makes it possible to learn the code base and the review process, while working with compose.

An example of just a feature like that would be a custom DNS: #3200 #1708
It is a long-requested feature, but to implement it you need to use compose and work within the existing code base.

If you want to work on something else, you have a wide selection to pick from in https://github.com/recloudstream/cloudstream/issues, but I would advise against implementing new features, since it is very easy to make mistakes when creating new features in a new code base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants