Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ea1bc2d
fix: use macos-13 and clean packs before workload install
mateof Dec 27, 2025
6007976
fix: use macos-15 and stable NuGet source
mateof Dec 27, 2025
83a9bec
fix: nuclear cleanup and rollback file for macOS workloads
mateof Dec 27, 2025
a342187
fix: maccatalyst min version 15.0, make builds independent
mateof Dec 27, 2025
604b66b
feat: separate builds by tag prefix (server-v*, app-v*, v*)
mateof Dec 27, 2025
ad452d9
fix: make Android APK signing conditional on keystore availability
mateof Dec 27, 2025
bc02267
fix: collect macOS .pkg files instead of looking for .app bundles
mateof Dec 27, 2025
859e548
fix: generate debug keystore when no release keystore is configured
mateof Dec 27, 2025
80bbdc2
fix: improve Android APK verification and Windows signing
mateof Dec 27, 2025
d42953d
fix: add keystore verification and improve Windows signing checks
mateof Dec 27, 2025
6207dd3
fix: prevent false track skip when seeking on network streams
mateof Dec 27, 2025
0f25f27
fix: increase LibVLC timeouts for streaming from Telegram
mateof Dec 27, 2025
0daeff5
chore: improve audio seek
mateof Dec 28, 2025
c91c3a8
chore: fix app
mateof Dec 28, 2025
e5d16db
chore: avoid pre-download from server when file is playing
mateof Dec 28, 2025
8f9df57
fix: use macos-14 runner and simplify MAUI workload installation
mateof Dec 28, 2025
ba2b7fa
fix: use macos-15 with Xcode selection for MAUI build
mateof Dec 28, 2025
6f03f5e
fix: filter in windows app
mateof Dec 28, 2025
e0ba1bc
fix: pin MAUI workload versions to avoid SDK 26.0 bug
mateof Dec 28, 2025
04591bb
feat: add search and filter params to url in server
mateof Dec 28, 2025
535981d
chore: pwa settings
mateof Dec 28, 2025
7bcaa77
fix: apply filter in lcoal folders
mateof Dec 29, 2025
5aef3ef
fix: fix sorted
mateof Dec 29, 2025
467f7c6
feat: add memory split before upload
mateof Dec 29, 2025
3d013fe
feat: add config memorysplit
mateof Dec 29, 2025
3d5da3e
chore: bump version
mateof Dec 30, 2025
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
340 changes: 270 additions & 70 deletions .github/workflows/buildrelease.yml

Large diffs are not rendered by default.

20 changes: 15 additions & 5 deletions .github/workflows/release-docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
name: Docker Image CI (Release)

# Triggers:
# - Push tags: server-v* OR v* (but NOT app-v*)
# Docker is only built for Server releases

on:
push:
tags: [ 'v*.*.*' ]
tags:
- 'v*.*.*'
- 'server-v*.*.*'

jobs:

build:

runs-on: ubuntu-latest
# Skip if it's an app-only release
if: ${{ !startsWith(github.ref_name, 'app-v') }}

steps:
- uses: actions/checkout@v4

- name: Extract version from tag
id: version
run: |
# Remove 'v' prefix from tag (v1.2.3 -> 1.2.3)
VERSION=${GITHUB_REF_NAME#v}
TAG=${GITHUB_REF_NAME}
# Remove 'server-v' or 'v' prefix
VERSION=${TAG#server-v}
VERSION=${VERSION#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from tag: $TAG"

- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v3
Expand Down
5 changes: 4 additions & 1 deletion TFMAudioApp/Controls/FilterPopup.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
StrokeThickness="0"
Padding="20"
MinimumWidthRequest="340"
MaximumWidthRequest="400">
MaximumWidthRequest="400"
MaximumHeightRequest="600">
<Border.StrokeShape>
<RoundRectangle CornerRadius="20"/>
</Border.StrokeShape>

<ScrollView>
<VerticalStackLayout Spacing="16">
<!-- Header -->
<Grid ColumnDefinitions="*,Auto">
Expand Down Expand Up @@ -141,5 +143,6 @@
Clicked="OnApplyClicked"/>
</Grid>
</VerticalStackLayout>
</ScrollView>
</Border>
</toolkit:Popup>
23 changes: 4 additions & 19 deletions TFMAudioApp/Controls/PlaylistPickerPopup.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
<CollectionView x:Name="PlaylistsCollection"
Grid.Row="2"
Margin="10,0"
SelectionMode="Single"
SelectionChanged="OnPlaylistSelected">
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<Border BackgroundColor="Transparent"
Expand All @@ -72,23 +71,9 @@
<Border.StrokeShape>
<RoundRectangle CornerRadius="8"/>
</Border.StrokeShape>
<Border.Behaviors>
<behaviors:TapFeedbackBehavior/>
</Border.Behaviors>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnPlaylistTapped" CommandParameter="{Binding}"/>
</Border.GestureRecognizers>
<Grid ColumnDefinitions="Auto,*,Auto">
<Label Text="🎵"
FontSize="24"
Expand Down
4 changes: 2 additions & 2 deletions TFMAudioApp/Controls/PlaylistPickerPopup.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ private void OnSearchTextChanged(object? sender, TextChangedEventArgs e)
}
}

private void OnPlaylistSelected(object? sender, SelectionChangedEventArgs e)
private void OnPlaylistTapped(object? sender, TappedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is Playlist playlist)
if (e.Parameter is Playlist playlist)
{
_selectedPlaylist = playlist;
Result = new PlaylistPickerResult
Expand Down
109 changes: 74 additions & 35 deletions TFMAudioApp/Services/AudioPlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class AudioPlayerService : IAudioPlayerService, IDisposable
private readonly Random _random = new();

private LibVLC? _libVLC;
private MediaPlayer? _mediaPlayer;
private LibVLCSharp.Shared.MediaPlayer? _mediaPlayer;
private Media? _currentMedia;
private System.Timers.Timer? _positionTimer;
private List<Track> _originalQueue = new();
Expand Down Expand Up @@ -80,32 +80,25 @@ public void Initialize()
// Initialize LibVLC core
Core.Initialize();

// Create LibVLC instance with audio-only optimizations and network settings
// Increased timeouts to handle server-side file downloads from Telegram
// Create LibVLC instance with minimal options for stability
_libVLC = new LibVLC(
"--no-video", // Disable video for audio-only playback
"--verbose=2", // Enable verbose logging for debugging
"--no-lua", // Disable Lua scripting
"--no-snapshot-preview", // No snapshot previews
"--network-caching=30000", // 30 seconds of network buffer (server may need to download from Telegram)
"--live-caching=30000", // 30 seconds for live streams
"--file-caching=10000", // 10 seconds file caching
"--http-reconnect", // Auto-reconnect on connection drops
"--http-continuous", // Enable continuous stream reading
"--sout-mux-caching=5000", // Output muxer caching
"--tcp-caching=30000", // TCP caching for slow connections
"--clock-jitter=0", // Reduce jitter sensitivity
"--clock-synchro=0" // Disable strict clock sync
"--no-video", // Disable video for audio-only playback
"--quiet", // Minimal logging
"--no-lua", // Disable Lua scripting
"--no-snapshot-preview" // No snapshot previews
);

// Log LibVLC messages for debugging
// Only log errors from LibVLC
_libVLC.Log += (s, e) =>
{
System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}");
if (e.Level == LogLevel.Error)
{
System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}");
}
};

// Create media player
_mediaPlayer = new MediaPlayer(_libVLC);
_mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC);
_mediaPlayer.Volume = (int)(_volume * 100);

// Setup event handlers
Expand Down Expand Up @@ -257,10 +250,41 @@ private void OnStopped(object? sender, EventArgs e)
_positionTimer?.Stop();
}

private bool _isSeeking;
private DateTime _lastSeekTime = DateTime.MinValue;

private void OnEndReached(object? sender, EventArgs e)
{
_positionTimer?.Stop();

// Check if this is a false "end reached" due to a failed seek on network stream
// If we were seeking recently (within 3 seconds) and position is not near the end, ignore this event
var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds;
if (timeSinceSeek < 3 && _mediaPlayer != null)
{
var currentPos = _mediaPlayer.Position;
var length = _mediaPlayer.Length;
var positionMs = currentPos * length;
var remainingMs = length - positionMs;

// If more than 5 seconds remaining, this is likely a false end-of-stream from failed seek
if (remainingMs > 5000)
{
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Ignoring false EndReached after seek (remaining: {remainingMs}ms)");
// Try to resume playback
MainThread.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(500);
if (_mediaPlayer != null && CurrentTrack != null)
{
System.Diagnostics.Debug.WriteLine("[AudioPlayer] Attempting to resume after false EndReached");
await PlayAtIndexAsync(CurrentIndex);
}
});
return;
}
}

// Handle end of track on main thread
MainThread.BeginInvokeOnMainThread(async () =>
{
Expand Down Expand Up @@ -297,7 +321,25 @@ private void OnError(object? sender, EventArgs e)
}

System.Diagnostics.Debug.WriteLine($"[AudioPlayer] LibVLC error occurred for track: {CurrentTrack?.FileName ?? "null"}");
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}");
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}, isSeeking: {_isSeeking}");

// If error occurred during seek on network stream, don't treat it as fatal
// Just ignore the seek and continue playing from current position
var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds;
if (timeSinceSeek < 3)
{
System.Diagnostics.Debug.WriteLine("[AudioPlayer] Error during seek - attempting to resume playback");
MainThread.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(500);
if (_mediaPlayer != null && CurrentTrack != null && State != PlaybackState.Playing)
{
// Restart the track from the beginning if we can't seek
await PlayAtIndexAsync(CurrentIndex);
}
});
return;
}

// Retry playback if under max retries (server might have been downloading)
if (_retryCount < MaxRetries && CurrentIndex >= 0 && CurrentIndex < Queue.Count)
Expand Down Expand Up @@ -550,24 +592,21 @@ public async Task PreviousAsync()

public async Task SeekAsync(TimeSpan position)
{
if (_mediaPlayer == null || _mediaPlayer.Length <= 0) return;
if (_mediaPlayer == null) return;

// Seeking is now allowed for all tracks (server supports progressive streaming with range requests)
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seeking to {position}");
var length = _mediaPlayer.Length;
if (length <= 0) return;

try
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
var positionRatio = (float)(position.TotalMilliseconds / _mediaPlayer.Length);
_mediaPlayer.Position = Math.Clamp(positionRatio, 0f, 1f);
});
}
catch (Exception ex)
_lastSeekTime = DateTime.UtcNow;

// Use Position (0.0 to 1.0 ratio) which is more reliable than Time
var positionRatio = (float)(position.TotalMilliseconds / length);
positionRatio = Math.Clamp(positionRatio, 0f, 0.99f);

await MainThread.InvokeOnMainThreadAsync(() =>
{
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seek failed: {ex.Message}");
// Don't propagate error - just ignore failed seek
}
_mediaPlayer.Position = positionRatio;
});
}

public async Task PlayAtIndexAsync(int index)
Expand Down
8 changes: 6 additions & 2 deletions TFMAudioApp/TFMAudioApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@
<!-- Platform minimum versions -->
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>

<!-- Android Signing Configuration (used during Release builds) -->
<!-- Android Package Format -->
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
<AndroidPackageFormat>apk</AndroidPackageFormat>
</PropertyGroup>

<!-- Android Signing Configuration (only if keystore is provided) -->
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release' and '$(ANDROID_KEYSTORE_PATH)' != ''">
<AndroidKeyStore>true</AndroidKeyStore>
<AndroidSigningKeyStore>$(ANDROID_KEYSTORE_PATH)</AndroidSigningKeyStore>
<AndroidSigningKeyAlias>$(ANDROID_KEY_ALIAS)</AndroidSigningKeyAlias>
Expand Down
23 changes: 20 additions & 3 deletions TelegramDownloader/Controllers/Mobile/MobileFileController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,17 @@ public async Task<IActionResult> BrowseTelegramFiles(string channelId, [FromQuer
items = items.Where(i => i.Name.ToLower().Contains(searchLower)).ToList();
}

// Sort
// Sort - folders always first, then apply sorting
items = (request.SortBy?.ToLower(), request.SortDescending) switch
{
("date", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.DateModified).ToList(),
("date", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.DateModified).ToList(),
("size", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Size).ToList(),
("size", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Size).ToList(),
("name", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Name).ToList(),
("name", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList(),
("type", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Type).ToList(),
("type", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Type).ToList(),
_ => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList()
};

Expand Down Expand Up @@ -238,7 +242,16 @@ public IActionResult BrowseLocalFiles([FromQuery] BrowseRequest request)
// Apply filter
if (!string.IsNullOrEmpty(request.Filter) && request.Filter.ToLower() != "all")
{
items = items.Where(i => i.IsFolder || i.Category.ToLower() == request.Filter.ToLower()).ToList();
var filterLower = request.Filter.ToLower();
if (filterLower == "audio_folders")
{
// audio_folders = show audio files and folders
items = items.Where(i => i.IsFolder || i.Category.ToLower() == "audio").ToList();
}
else
{
items = items.Where(i => i.IsFolder || i.Category.ToLower() == filterLower).ToList();
}
}

// Apply search
Expand All @@ -248,13 +261,17 @@ public IActionResult BrowseLocalFiles([FromQuery] BrowseRequest request)
items = items.Where(i => i.Name.ToLower().Contains(searchLower)).ToList();
}

// Sort
// Sort - folders always first, then apply sorting
items = (request.SortBy?.ToLower(), request.SortDescending) switch
{
("date", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.DateModified).ToList(),
("date", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.DateModified).ToList(),
("size", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Size).ToList(),
("size", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Size).ToList(),
("name", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Name).ToList(),
("name", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList(),
("type", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Type).ToList(),
("type", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Type).ToList(),
_ => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList()
};

Expand Down
Loading
Loading