Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8fd2d25
Merge pull request #89 from mateof/sync/main-to-develop-v3.6.2
mateof Feb 9, 2026
682d9d8
fix: fix folder creation logic to ensure CurrentFolder is valid befor…
mateof Apr 14, 2026
8baf50c
fix: solve upload to empty folder and upload to root folder
mateof Apr 14, 2026
0c50bd0
fix: solve folder problem
mateof Apr 14, 2026
f991d16
Sync develop with main after v3.6.3 (#93)
github-actions[bot] Apr 14, 2026
d1c60e1
fix: update MongoDB.Driver and System.IO.Hashing package versions
mateof May 23, 2026
f37761c
feat: wire progressive download cache into tfm streaming endpoint
mateof Jul 12, 2026
0b8072b
Merge pull request #94 from mateof/feat/progressive-streaming-cache
mateof Jul 12, 2026
128776a
feat: configurable STRM streaming mode with progressive disk cache
mateof Jul 13, 2026
2ca1bb8
Merge pull request #95 from mateof/feature/configurable-strm-streamin…
mateof Jul 13, 2026
13bf92f
fix: show progress and honor cancel for progressive cache downloads
mateof Jul 13, 2026
6e61d7d
Merge pull request #96 from mateof/feature/configurable-strm-streamin…
mateof Jul 13, 2026
e64b8eb
feat: audio transcoding endpoint for offline downloads (MP3/AAC)
mateof Jul 19, 2026
543e66f
Merge pull request #97 from mateof/feat/audio-transcode-downloads
mateof Jul 19, 2026
05fdae6
fix: reliable live task updates in Tasks Manager
mateof Jul 21, 2026
893269c
feat: configurable parallel chunk transfers for faster downloads/uploads
mateof Jul 21, 2026
e4e29dc
Merge pull request #98 from mateof/fix/tasks-live-refresh
mateof Jul 21, 2026
a2129ee
Merge pull request #99 from mateof/feat/configurable-parallel-transfers
mateof Jul 21, 2026
11181c1
feat: multi-connection downloads to bypass per-connection speed limit
mateof Jul 21, 2026
fbf90db
Merge pull request #100 from mateof/feat/multi-connection-downloads
mateof Jul 21, 2026
1c4a832
fix: enable multi-connection mode on the file-manager download path
mateof Jul 21, 2026
33807ab
Merge pull request #101 from mateof/fix/multi-connection-download-path
mateof Jul 21, 2026
51ec46b
fix: bootstrap download pool via neighbor DC to avoid DC_ID_INVALID
mateof Jul 21, 2026
370da09
fix: bootstrap download pool via neighbor DC to avoid DC_ID_INVALID (…
mateof Jul 21, 2026
39e2b52
fix: finalize pool client login and drop probe RPCs after import
mateof Jul 21, 2026
c78ba87
Merge pull request #103 from mateof/fix/pool-imported-auth
mateof Jul 21, 2026
1f87e13
fix: tolerate re-import on an already-authorized pool session key
mateof Jul 21, 2026
b453f1f
fix: tolerate re-import on an already-authorized pool session key (#104)
mateof Jul 21, 2026
d9e673a
feat: pool clients clone the main session instead of importing auth
mateof Jul 21, 2026
51d0537
Merge pull request #105 from mateof/feat/pool-session-clone
mateof Jul 22, 2026
09f60c9
fix: clone the session despite the live file lock
mateof Jul 22, 2026
1179efb
fix: clone the session despite the live file lock (#106)
mateof Jul 22, 2026
895097b
feat: pipeline file parts within each download connection (#107)
mateof Jul 22, 2026
7df75a6
feat: real sea-wave effect on the top bar download/upload buttons
mateof Jul 22, 2026
0715286
Merge pull request #108 from mateof/feature/water-fill-wave-effect
mateof Jul 22, 2026
5adb1eb
feat: auth guard before rendering with return to the requested page
mateof Jul 22, 2026
b6874fb
Merge pull request #109 from mateof/feature/auth-guard-return-url
mateof Jul 22, 2026
f86d1e2
feat: QR code login option in the web login page
mateof Jul 22, 2026
8ff954e
Merge pull request #110 from mateof/feature/qr-login
mateof Jul 22, 2026
e432b95
feat: live refresh in the download/upload/task info modals
mateof Jul 22, 2026
6d937ea
Merge pull request #111 from mateof/feature/live-info-modals
mateof Jul 22, 2026
faaa062
Sync develop with main after v3.7.0 (#115)
github-actions[bot] Jul 22, 2026
4e0eec0
feat(api): add modular v1 REST + SignalR API and make mobile app buil…
mateof Jul 23, 2026
1d73f23
fix(api/v1/channels): honour onlySaved and populate HasDatabase
mateof Jul 24, 2026
ea34ce7
Merge pull request #117 from mateof/fix/api-v1-channels-saved-filter
mateof Jul 24, 2026
08feb98
feat(webdav): native read/write WebDAV endpoint; retire Python proxy
mateof Jul 24, 2026
75eb175
Merge pull request #118 from mateof/feat/webdav-native-hyperbackup
mateof Jul 24, 2026
9a13d0f
fix(docker): drop retired Python WebDAV proxy from the image
mateof Jul 24, 2026
f88d493
fix(docker): drop retired Python WebDAV proxy from the image (#119)
mateof Jul 24, 2026
ff9e221
fix(build): remove orphaned WebDavInfo.razor.css scoped stylesheet
mateof Jul 24, 2026
a19110d
feat(webdav): channel header button with the WebDAV URL of the curren…
mateof Jul 24, 2026
a0e4b64
feat(channels): hide channels with a "show hidden channels" setting
mateof Jul 25, 2026
b40ee73
fix(channels): hide channels in the Folders tab and keep folders expa…
mateof Jul 25, 2026
30d4784
Document the Android phone and TV clients in the README
mateof Jul 25, 2026
0cbdc62
Merge main into develop to resolve the PR #120 conflict
mateof Jul 26, 2026
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
26 changes: 17 additions & 9 deletions .github/workflows/buildrelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ name: Build and Release
# Triggers:
# - Release published with tags:
# - server-v* : Build only Server
# - app-v* : Build only TFMAudioApp (Android, Windows, macOS)
# - v* : Build everything
# - Manual workflow dispatch with checkboxes
# - app-v* : Build only TFMAudioApp (Android, Windows, macOS) — on demand
# - v* : Build only Server (mobile apps are NOT built by default)
# - Manual workflow dispatch with checkboxes (build any target on demand)
#
# NOTE: Mobile apps (Android/Windows/macOS) are intentionally NOT built for a
# plain "v*" release. Build them on demand when they actually change, either
# with an "app-v*" release tag or via the manual workflow_dispatch checkboxes.
# See docs/releases.md for the full policy.

on:
release:
Expand Down Expand Up @@ -128,10 +133,11 @@ jobs:
build-android:
name: Build Android APK
runs-on: ubuntu-latest
# Run if: manual with build_android OR release with app-v* or v* (but not server-v*)
# On demand only: manual with build_android OR release with an app-v* tag.
# A plain v* release does NOT build the mobile apps.
if: |
(github.event_name == 'workflow_dispatch' && inputs.build_android) ||
(github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v'))))
(github.event_name == 'release' && startsWith(github.event.release.tag_name, 'app-v'))
steps:
- name: '📄 Checkout'
uses: actions/checkout@v4
Expand Down Expand Up @@ -317,10 +323,11 @@ jobs:
build-windows:
name: Build Windows App
runs-on: windows-2022
# Run if: manual with build_windows OR release with app-v* or v* (but not server-v*)
# On demand only: manual with build_windows OR release with an app-v* tag.
# A plain v* release does NOT build the mobile apps.
if: |
(github.event_name == 'workflow_dispatch' && inputs.build_windows) ||
(github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v'))))
(github.event_name == 'release' && startsWith(github.event.release.tag_name, 'app-v'))
steps:
- name: '📄 Checkout'
uses: actions/checkout@v4
Expand Down Expand Up @@ -449,10 +456,11 @@ jobs:
build-macos:
name: Build macOS App
runs-on: macos-15
# Run if: manual with build_macos OR release with app-v* or v* (but not server-v*)
# On demand only: manual with build_macos OR release with an app-v* tag.
# A plain v* release does NOT build the mobile apps.
if: |
(github.event_name == 'workflow_dispatch' && inputs.build_macos) ||
(github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v'))))
(github.event_name == 'release' && startsWith(github.event.release.tag_name, 'app-v'))
steps:
- name: '📄 Checkout'
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
TelegramDownloader/userData.json
TelegramDownloader/.claude/
.claude/
plans/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,46 @@ services:
- When you want, you can download the files again to a location on your local computer, selecting the files or folders and clicking the `Download to Local` button.


## Android apps

Both apps are native Kotlin + Jetpack Compose clients built on the server's REST API v1
(`/api/v1`), documented in [`docs/api`](docs/api). Point them at your server address and
API key and they work over the same Telegram session as the web.

### Phone and tablet — [tfm-android-app](https://github.com/mateof/tfm-android-app)

Full file manager on the go:

- Telegram login from the app, by **QR** or phone number (with 2FA).
- **Channels**: saved / all / favourites, search, statistics, create a channel, join by
invitation, leave, and build or refresh a channel index choosing which media types to
scan.
- **File browser** with breadcrumbs, filters, recursive search, multi-select, folder
creation, rename, copy/move, delete, **upload from the device** and download to the
server or to the device.
- **Server local storage**: browse, upload, send to a Telegram channel without
re-uploading the bytes, and clear the streaming cache.
- **Live transfers** over the `/hubs/transfers` SignalR hub: speed, progress and queues,
with global and per-item pause, resume, cancel and retry.
- **Background audio player** (Media3 + MediaSession) with a mini player and server-side
playlists, plus **video streaming** for channel and local files.
- Server settings: simultaneous downloads, parallel chunks, connections per download.

### Android TV and Fire TV — [tfm-android-tv-app](https://github.com/mateof/tfm-android-tv-app)

A D-pad first client focused on watching the videos you keep in your channels:

- Channels split into mine, shared, favourites, Telegram chat folders and all, with a
name search.
- Folder navigation inside a channel, an **all videos** view and a **messages** view,
each sortable by name, date or size.
- Playback with the built-in player (ExoPlayer plus FFmpeg software decoders, so MKV or
AVI with AC3/DTS play fine), VLC, any other installed player or the system default.
- Updates itself from GitHub Releases, and runs on Android 6.0, which covers Fire TV
sticks from 2015 onwards.

Both repositories publish a signed APK as a GitHub Release on every push to `main`.

## Music player

<img width="598" height="889" alt="Music player" src="https://github.com/user-attachments/assets/c4842c34-ea8f-4664-b22c-93b43a9aaf02" />
Expand Down
4 changes: 3 additions & 1 deletion TelegramDownloader/Configuration/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
"mongo_connection_string": "",
"avoid_checking_certificate": false,
"open_browser_on_startup": true,
"mobile_api_key": "my_api_key"
"mobile_api_key": "my_api_key",
"webdav_user": "",
"webdav_password": ""
}
61 changes: 61 additions & 0 deletions TelegramDownloader/Controllers/Api/V1/ApiV1ControllerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using TelegramDownloader.Models.Api;

namespace TelegramDownloader.Controllers.Api.V1
{
/// <summary>
/// Shared plumbing for every v1 controller: consistent envelopes, consistent
/// status codes and a helper to build absolute URLs behind a reverse proxy.
/// </summary>
[ApiController]
[Produces("application/json")]
[ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status500InternalServerError)]
public abstract class ApiV1ControllerBase : ControllerBase
{
/// <summary>
/// Absolute base URL of this server as seen by the client, honouring
/// <c>X-Forwarded-Proto</c>/<c>X-Forwarded-Host</c> (the app enables
/// forwarded headers at startup).
/// </summary>
protected string BaseUrl => $"{Request.Scheme}://{Request.Host}";

protected IActionResult OkResult<T>(T data, string? message = null) =>
Ok(ApiResult<T>.Ok(data, message));

protected IActionResult OkPaged<T>(T data, PageInfo page) =>
Ok(ApiResult<T>.Ok(data, page));

protected IActionResult OkEmpty(string? message = null) =>
Ok(ApiResult.Done(message));

protected IActionResult BadRequestResult(string message, string code = ApiErrorCodes.InvalidRequest, string? detail = null) =>
BadRequest(ApiResult.Fail(code, message, detail));

protected IActionResult NotFoundResult(string message, string code = ApiErrorCodes.NotFound) =>
NotFound(ApiResult.Fail(code, message));

protected IActionResult ConflictResult(string message, string code = ApiErrorCodes.Conflict) =>
Conflict(ApiResult.Fail(code, message));

protected IActionResult ForbiddenResult(string message) =>
StatusCode(StatusCodes.Status403Forbidden, ApiResult.Fail(ApiErrorCodes.Forbidden, message));

protected IActionResult ErrorResult(string message, Exception? ex = null, string code = ApiErrorCodes.InternalError) =>
StatusCode(StatusCodes.Status500InternalServerError, ApiResult.Fail(code, message, ex?.Message));

protected IActionResult UnavailableResult(string message, string code = ApiErrorCodes.ServiceUnavailable) =>
StatusCode(StatusCodes.Status503ServiceUnavailable, ApiResult.Fail(code, message));

/// <summary>
/// Applies in-memory paging to an already materialised list and returns
/// both the page and its metadata.
/// </summary>
protected static (List<T> Items, PageInfo Page) Paginate<T>(IReadOnlyList<T> source, PagedQuery query)
{
var page = PageInfo.Create(query.Page, query.PageSize, source.Count);
var items = source.Skip((query.Page - 1) * query.PageSize).Take(query.PageSize).ToList();
return (items, page);
}
}
}
Loading
Loading