Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Builds NX-torrent-player.nro in the devkitPro image.
#
# - Pull requests and manual runs: build, keep the .nro as a run artifact.
# - A pushed tag vX.Y.Z: build, then open a DRAFT release with the .nro
# attached. Publishing it is a manual step on purpose: the in-app updater
# offers the latest *published* release to every user, and shows the release
# body as the changelog -- write it before pressing Publish.
#
# git tag v0.5.1 && git push origin v0.5.1

name: Build

on:
push:
tags: ["v*"]
pull_request:
workflow_dispatch:

permissions:
contents: write # creating the draft release

jobs:
# Offline engine tests (pctest/test_suite.c): mocked trackers and DHT, a mock
# peer on loopback. No network, so they are safe to gate on.
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install libcurl
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
- name: Run
run: sh pctest/run_tests.sh

nro:
needs: tests
runs-on: ubuntu-latest
container: devkitpro/devkita64:latest
steps:
- uses: actions/checkout@v4

# The updater compares the tag with the version compiled into the .nro.
# A tag that disagrees with CMakeLists.txt would ship a build that keeps
# offering itself as an update forever (or never updates again).
- name: Check the tag matches the version
if: startsWith(github.ref, 'refs/tags/')
run: |
v() { sed -n "s/^set(VERSION_$1 \"\([0-9]*\)\")/\1/p" CMakeLists.txt; }
version="$(v MAJOR).$(v MINOR).$(v ALTER)"
tag="${GITHUB_REF_NAME#v}"
echo "CMakeLists.txt: $version, tag: $tag"
if [ "$version" != "$tag" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match VERSION_* ($version) in CMakeLists.txt"
exit 1
fi

- name: Install portlibs
run: |
dkp-pacman -Sy --needed --noconfirm \
switch-libmpv switch-curl switch-libwebp switch-mbedtls \
switch-glfw switch-mesa

# TMDB_API_KEY (a repository secret, optional): the catalog key a release
# ships with. Without it users enter their own in Options -> Network.
- name: Configure
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
run: cmake -B build -DPLATFORM_SWITCH=ON -DCMAKE_BUILD_TYPE=Release -DTMDB_API_KEY="$TMDB_API_KEY"

- name: Build
run: cmake --build build --target NX-torrent-player.nro -j "$(nproc)"

# The .elf rides along so a crash report's offsets can be resolved against
# the exact build (aarch64-none-elf-addr2line -Cfe ... <offset>).
- uses: actions/upload-artifact@v4
with:
name: NX-torrent-player
path: |
build/NX-torrent-player.nro
build/NX-torrent-player.elf
if-no-files-found: error

- name: Draft the release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
draft: true
name: ${{ github.ref_name }}
files: build/NX-torrent-player.nro
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
# Build output
/build/
/build-ninja/
/build-pc/
/pc_deps/
/NX-torrent-player.nro
/.updatetest/
/NX-torrent-player.elf
/NX-torrent-player.nacp
/pctest/enginetest
*.o
/pctest/test_suite
/pctest/test_suite.exe
/pctest/test_httpsrc
/pctest/test_httpsrc.exe
/pctest/test_net
/pctest/test_net.exe
/pctest/test_tmdb
/pctest/test_tmdb.exe
/pctest/test_release
/pctest/test_release.exe
/pctest/test_library
/pctest/test_library.exe
25 changes: 22 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,22 @@ rewrites the container paths.
## Architecture

- `app/` — the UI (C++, borealis): `main` (browser/tabs), `player` (mpv view +
loading screen + seek), `stremio` (API, library, artwork), `browse` (the
Stremio drill-down), `settings`, `config` (config.json)
loading screen + seek; torrents, direct http(s) links, TorrServer), `stremio`
(API, library, artwork), `browse` (the Stremio drill-down), `settings`,
`config` (config.json), `http` (curl + proxy), `torrserver`, `i18n` (+
`i18n_ru.cpp`, one table per language), `home` (the TMDB home tab), `card`
(film/series card), `tmdb` (client + SD cache), `content` (the model the
catalog screens share), `widgets` (strips, poster/wide cards, chips, shades),
`sources` (the release list for a film/episode: Jackett + Stremio addons),
`release` (release-name parsing, ranking, episode-in-pack matching),
`torrent_play` (open a Jackett result or magnet and pick the file to play),
`library` (bookmarks + per-title/per-episode progress, library.json),
`bookmarks` (the Bookmarks tab)
- `engine/` — the torrent engine (C): `torrentfs` (piece picking, cache, the
streaming read), `peer`, `swarm`, `torrent`, `udp_tracker`, `dht`, `magnet`,
`stream` (the devoptab mpv reads through), plus vendored libutp (`utp*.cpp`)
`stream` (the mpv `stream_cb` protocols: `torrent://` and `nxhttps://`),
`httpsrc` (https read through curl range requests), plus vendored libutp
(`utp*.cpp`)
- `include/` — libutp headers, paired with `engine/utp*.cpp`
- `assets/` — app icon, logos, list icons (copied into the nro's romfs)
- `library/borealis/` — vendored UI framework, locally patched: see its
Expand Down Expand Up @@ -75,6 +86,14 @@ The entry point is `int main()` in `app/main.cpp`.
focus parking in `StremioTab::onAuthenticated`.
- The Switch outputs stereo: mpv is forced to `audio-channels=stereo` or 5.1
dialogue (centre channel) is lost.
- The portlibs **FFmpeg has no `https` protocol** (only file, http, ftp, tcp,
udp, rtmp, tls, httpproxy; `tls` is libnx's ssl service). mpv opens `http://`
and HLS over it itself; an `https://` URL is played as `nxhttps://` through
`engine/httpsrc.c` (curl, our CA bundle, Range requests). HLS over https is
therefore not supported yet. No `crypto` protocol either (AES-128 HLS).
- **Space Grotesk (the primary UI font) has no Cyrillic**: in Russian the app
turns it off before `Application::init()` (`SwitchFontLoader::useBundledFont`,
a local borealis patch) so the UI is the system font throughout.

## Debugging on device

Expand Down
56 changes: 48 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ cmake_dependent_option(USE_SYSTEM_FMT "" OFF "NOT USE_SHARED_LIB" ON)
cmake_dependent_option(USE_SYSTEM_TINYXML2 "" OFF "NOT USE_SHARED_LIB" ON)
cmake_dependent_option(USE_SYSTEM_TWEENY "" OFF "NOT USE_SHARED_LIB" ON)

set(CMAKE_NO_SYSTEM_FROM_IMPORTED ON)

include(${BOREALIS_LIBRARY}/cmake/toolchain.cmake)

project(NX-torrent-player)
set(VERSION_MAJOR "0")
set(VERSION_MINOR "5")
set(VERSION_ALTER "0")
set(VERSION_MINOR "6")
set(VERSION_ALTER "1")
set(PROJECT_AUTHOR "shodowlo")
# On-device app name shown in the Homebrew Menu.
set(APP_TITLE "NX Torrent Player")
Expand All @@ -30,9 +32,18 @@ set(PROJECT_TITLEID "0x010000000000A123")
# entirely -- a local HTTP server serving a fake release JSON is how the update
# flow gets tested without publishing one:
# cmake . -DUPDATE_API_URL=http://192.168.1.20:8000/latest.json
set(UPDATE_REPO "shodowlo/NX-torrent-player" CACHE STRING "GitHub owner/name for updates")
# This fork publishes its own releases: pointing at upstream made the updater
# replace the fork with an upstream build (losing Jackett, History, ...).
set(UPDATE_REPO "Langegen/NX-torrent-player" CACHE STRING "GitHub owner/name for updates")
set(UPDATE_API_URL "https://api.github.com/repos/${UPDATE_REPO}/releases/latest"
CACHE STRING "Release API endpoint the updater polls")
# A tag's release (its changelog), the tag appended. Same repo as above.
set(UPDATE_TAG_URL "https://api.github.com/repos/${UPDATE_REPO}/releases/tags/"
CACHE STRING "Release-by-tag endpoint, the tag is appended")
# The TMDB key a build ships with (a v3 key or a v4 read token), used when the
# user has not entered one in Options. Empty by default: the home screen then
# asks for one. CI can pass it from a secret: -DTMDB_API_KEY=...
set(TMDB_API_KEY "" CACHE STRING "Default TMDB API key / read token")
set(PROJECT_ICON ${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.jpg)
set(PROJECT_RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/library/borealis/resources)

Expand All @@ -42,7 +53,9 @@ find_package(Threads REQUIRED)
list(APPEND APP_PLATFORM_LIB ${CMAKE_THREAD_LIBS_INIT})

find_package(PkgConfig REQUIRED)
set(PKG_CONFIG_USE_STATIC_LIBS ON) # Switch links everything statically
if (PLATFORM_SWITCH)
set(PKG_CONFIG_USE_STATIC_LIBS ON) # Switch links everything statically
endif ()
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
pkg_check_modules(CURL REQUIRED IMPORTED_TARGET libcurl)
# Metahub serves some Stremio posters as WebP, which nanovg's stb_image cannot
Expand All @@ -58,10 +71,12 @@ set(ENGINE_SRC
${ENGINE_DIR}/bencode.c ${ENGINE_DIR}/torrent.c ${ENGINE_DIR}/torrentfs.c
${ENGINE_DIR}/peer.c ${ENGINE_DIR}/swarm.c ${ENGINE_DIR}/magnet.c
${ENGINE_DIR}/udp_tracker.c ${ENGINE_DIR}/dht.c ${ENGINE_DIR}/dhtclient.c
${ENGINE_DIR}/udpdiag.c ${ENGINE_DIR}/stream.c
${ENGINE_DIR}/udpdiag.c ${ENGINE_DIR}/stream.c ${ENGINE_DIR}/httpsrc.c
${ENGINE_DIR}/utp.cpp ${ENGINE_DIR}/utp_utils.cpp ${ENGINE_DIR}/utpbridge.cpp)
# libutp needs -DPOSIX for its socket types (SOCKADDR_STORAGE etc).
set_source_files_properties(${ENGINE_SRC} PROPERTIES COMPILE_DEFINITIONS "POSIX")
# libutp needs -DPOSIX for its socket types (SOCKADDR_STORAGE etc) on Switch.
if (PLATFORM_SWITCH)
set_source_files_properties(${ENGINE_SRC} PROPERTIES COMPILE_DEFINITIONS "POSIX")
endif ()
list(APPEND MAIN_SRC ${ENGINE_SRC})

if (PLATFORM_SWITCH)
Expand All @@ -82,7 +97,9 @@ target_compile_options(${PROJECT_NAME} PRIVATE -Wformat -Werror=format)
# Options, and is what the updater compares against the latest tag.
target_compile_definitions(${PROJECT_NAME} PRIVATE
APP_VERSION="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_ALTER}"
UPDATE_API_URL="${UPDATE_API_URL}")
UPDATE_API_URL="${UPDATE_API_URL}"
UPDATE_TAG_URL="${UPDATE_TAG_URL}"
TMDB_API_KEY="${TMDB_API_KEY}")

if (PLATFORM_SWITCH)
# Bundle borealis' resources into the nro's romfs (borealis reads them from
Expand Down Expand Up @@ -121,6 +138,29 @@ if (PLATFORM_SWITCH)

endif ()

if (NOT PLATFORM_SWITCH)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_RESOURCES} ${CMAKE_BINARY_DIR}/resources
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/NX-torrent-player-bg-rounded.png ${CMAKE_BINARY_DIR}/resources/NX-torrent-player-bg-rounded.png
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/video-icon.png ${CMAKE_BINARY_DIR}/resources/video-icon.png
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/magnet-icon.png ${CMAKE_BINARY_DIR}/resources/magnet-icon.png
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/stremio-icon.png ${CMAKE_BINARY_DIR}/resources/stremio-icon.png
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/local-tab-icon.png ${CMAKE_BINARY_DIR}/resources/local-icon.png
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/SpaceGrotesk-Medium.ttf ${CMAKE_BINARY_DIR}/resources/SpaceGrotesk-Medium.ttf
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/cacert.pem ${CMAKE_BINARY_DIR}/resources/cacert.pem
)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/pc_deps/mpv/libmpv-2.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/pc_deps/mpv/libmpv-2.dll ${CMAKE_BINARY_DIR}/libmpv-2.dll
)
endif ()
endif ()

if (NOT PLATFORM_SWITCH)
target_include_directories(${PROJECT_NAME} BEFORE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/compat)
endif ()

target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/app
${CMAKE_CURRENT_SOURCE_DIR}/engine
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ Whether you stream through **Stremio** catalogs and addons, search across dozens
- 🕒 **Watch History & Resume Playback** — Dedicated **History** tab tracking your recently watched videos across all sources (Local, Jackett, Stremio). Automatically remembers your playback position so you can resume right where you left off.
- 📂 **Multi-File Selection & Natural Sorting** — Smart file picker for torrents and magnets containing multiple videos with natural alphanumeric episode sorting (e.g., Episode 1, 2, ..., 10 instead of 1, 10, 2).
- ⚡ **Enhanced Engine & Tracker Pooling** — Background announce worker pool supporting concurrent announces across 30+ trackers, asynchronous metadata resolution with cancelable progress dialogs, and robust DHT peer search fallback.
- 🧪 **Desktop Test Suite** — Standalone PC test suite (`pctest/`) with custom SHA-1 compatibility layer to validate the torrent engine without needing a physical Switch or devkitPro toolchain.
- 🏠 **Home Screen & Title Cards (TMDB)** — A Home tab in the style of Lampa: a hero banner, *Continue watching*, and rows of trending, popular and top-rated films and shows from TMDB in your language; each title opens a card with backdrop, rating, genres, synopsis, cast, seasons and episodes, and finds sources through your Stremio addons or Jackett. Needs a free TMDB API key (**Options → Network → TMDB**); answers are cached on the SD card, so the home screen opens instantly and offline.
- 📚 **Library & Bookmarks** — Bookmark any title as *Watching*, *Planned*, *Completed* or *Dropped* (a **Bookmarks** tab lists them). Progress is kept per film and per episode, tied to the title rather than the file, so it survives picking another release; *Continue watching* on Home resumes in one press, and when an episode ends the next one opens from the same season pack. Export / import the whole library as one file.
- 🎯 **Smart Source Picking** — A title's *Watch*, *Torrents* or an episode opens one list of releases from Jackett (searched by the localized and the original title) and your Stremio addons. Release names are read into quality, source, codec, HDR / 10-bit, voice-overs and season / episode scope; releases for another episode are hidden, and the one that plays best on a Switch (1080p H.264 over 4K HEVC HDR, seeded, sensibly sized) comes first as *best choice*. Quality filters, X cycles the order. Season packs open straight on the right episode, and a show remembers the dub you picked.
- 🔗 **Direct Video Links** — Stremio addon streams that are plain `http(s)` URLs (debrid services and the like) now play, with seeking. `https` goes through the app's own curl with certificate checks, since the Switch's FFmpeg has no `https` of its own.
- 🖥️ **TorrServer** — Set your TorrServer address in **Options → Network** and every torrent (Local, Jackett, Stremio) is streamed by that server and played on the console as a regular video stream.
- 🌐 **Proxy** — An HTTP or SOCKS proxy for catalogs, addons, posters and `https` video links (**Options → Network**); servers on your local network are always reached directly.
- 🇷🇺 **Russian Interface** — A full Russian translation, switchable in **Options → General → Language**.
- 🧪 **Desktop Test Suite** — Offline PC tests (`pctest/`) for the torrent engine, `https` range reads and TorrServer links, with mock trackers, peers and servers on loopback — no physical Switch, devkitPro toolchain or network needed.

### 🎬 Core Features
- **True Sequential Streaming** — Starts playing almost immediately; the engine streams ahead of the playhead and automatically re-centers when you seek.
Expand Down Expand Up @@ -162,12 +169,19 @@ docker run --rm -v st_build:/build -v "$PWD":/project \
> On Windows (using Git Bash or MSYS2), prefix docker commands with `MSYS_NO_PATHCONV=1` to prevent path conversions.

### Desktop Engine Tests (PC)
You can build and run the standalone torrent engine tests on Linux/macOS or WSL:
The offline test suites need only gcc/g++ and libcurl headers (Linux, macOS, WSL or
MSYS2). Trackers and the DHT are mocked and a mock peer serves the metadata on
loopback, so they need no network; CI runs them on every pull request:
```sh
sh pctest/run_tests.sh
```

`enginetest` runs the real engine against a live swarm, for measuring download
behaviour (needs libmbedtls and zlib as well):
```sh
# Requires gcc, libcurl, libmbedtls, and zlib development packages
cd pctest
./build.sh
./enginetest
./enginetest <file.torrent | magnet:?...> [seconds]
```

---
Expand Down
Loading
Loading