Skip to content

Commit 1f4e5f7

Browse files
authored
internal: Develop to main (#54)
* fix: use macos-13 and clean packs before workload install * fix: use macos-15 and stable NuGet source * fix: nuclear cleanup and rollback file for macOS workloads * fix: maccatalyst min version 15.0, make builds independent * feat: separate builds by tag prefix (server-v*, app-v*, v*) * fix: make Android APK signing conditional on keystore availability - Separate AndroidPackageFormat from signing configuration in csproj - AndroidKeyStore=true now only set when ANDROID_KEYSTORE_PATH is provided - Workflow uses step outputs to properly detect signing availability - Unsigned builds explicitly pass -p:AndroidKeyStore=false - Fixes "package appears to be invalid" error when installing unsigned APKs * fix: collect macOS .pkg files instead of looking for .app bundles CreatePackage=true generates .pkg installer files, not .app bundles. Updated the packaging step to find and copy .pkg files to releases. * fix: generate debug keystore when no release keystore is configured Android requires ALL APKs to be signed to be installed. When no release keystore secrets are configured, generate a temporary debug keystore using keytool to sign the APK. This fixes "App not installed as package appears to be invalid" error. * fix: improve Android APK verification and Windows signing Android: - Add Android SDK tools to PATH for apksigner - Add APK validation (zip integrity check) - Add APK signature verification - Prefer signed APK over unsigned - Add more diagnostic output Windows: - Check both certificate AND password before attempting to sign - Test signing on one file first to validate password - Skip gracefully if password is incorrect - Remove continue-on-error to fail fast on real errors * fix: add keystore verification and improve Windows signing checks * fix: prevent false track skip when seeking on network streams - Detect false EndReached events after seek attempts - If seek fails on unbuffered stream, resume playback instead of skipping - Handle seek errors gracefully on network streams - Add more debug logging for seek operations * fix: increase LibVLC timeouts for streaming from Telegram - Increase network-caching from 30s to 60s for Telegram download delays - Increase tcp-caching to 60s for slow connections - Add prefetch-buffer-size and prefetch-read-size options - Previous commit: detect false EndReached after seeks * chore: improve audio seek * chore: fix app * chore: avoid pre-download from server when file is playing * fix: use macos-14 runner and simplify MAUI workload installation * fix: use macos-15 with Xcode selection for MAUI build * fix: filter in windows app * fix: pin MAUI workload versions to avoid SDK 26.0 bug * feat: add search and filter params to url in server fix: solve add to playlist issue * chore: pwa settings * fix: apply filter in lcoal folders * fix: fix sorted * feat: add memory split before upload * feat: add config memorysplit * chore: bump version
1 parent bd70a50 commit 1f4e5f7

19 files changed

Lines changed: 866 additions & 226 deletions

.github/workflows/buildrelease.yml

Lines changed: 270 additions & 70 deletions
Large diffs are not rendered by default.

.github/workflows/release-docker-image.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
name: Docker Image CI (Release)
22

3+
# Triggers:
4+
# - Push tags: server-v* OR v* (but NOT app-v*)
5+
# Docker is only built for Server releases
6+
37
on:
48
push:
5-
tags: [ 'v*.*.*' ]
9+
tags:
10+
- 'v*.*.*'
11+
- 'server-v*.*.*'
612

713
jobs:
814

915
build:
10-
1116
runs-on: ubuntu-latest
17+
# Skip if it's an app-only release
18+
if: ${{ !startsWith(github.ref_name, 'app-v') }}
1219

1320
steps:
1421
- uses: actions/checkout@v4
1522

1623
- name: Extract version from tag
1724
id: version
1825
run: |
19-
# Remove 'v' prefix from tag (v1.2.3 -> 1.2.3)
20-
VERSION=${GITHUB_REF_NAME#v}
26+
TAG=${GITHUB_REF_NAME}
27+
# Remove 'server-v' or 'v' prefix
28+
VERSION=${TAG#server-v}
29+
VERSION=${VERSION#v}
2130
echo "version=$VERSION" >> $GITHUB_OUTPUT
22-
echo "Extracted version: $VERSION"
31+
echo "tag=$TAG" >> $GITHUB_OUTPUT
32+
echo "Extracted version: $VERSION from tag: $TAG"
2333
2434
- name: 'Login to GitHub Container Registry'
2535
uses: docker/login-action@v3

TFMAudioApp/Controls/FilterPopup.xaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
StrokeThickness="0"
1010
Padding="20"
1111
MinimumWidthRequest="340"
12-
MaximumWidthRequest="400">
12+
MaximumWidthRequest="400"
13+
MaximumHeightRequest="600">
1314
<Border.StrokeShape>
1415
<RoundRectangle CornerRadius="20"/>
1516
</Border.StrokeShape>
1617

18+
<ScrollView>
1719
<VerticalStackLayout Spacing="16">
1820
<!-- Header -->
1921
<Grid ColumnDefinitions="*,Auto">
@@ -141,5 +143,6 @@
141143
Clicked="OnApplyClicked"/>
142144
</Grid>
143145
</VerticalStackLayout>
146+
</ScrollView>
144147
</Border>
145148
</toolkit:Popup>

TFMAudioApp/Controls/PlaylistPickerPopup.xaml

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@
6161
<CollectionView x:Name="PlaylistsCollection"
6262
Grid.Row="2"
6363
Margin="10,0"
64-
SelectionMode="Single"
65-
SelectionChanged="OnPlaylistSelected">
64+
SelectionMode="None">
6665
<CollectionView.ItemTemplate>
6766
<DataTemplate>
6867
<Border BackgroundColor="Transparent"
@@ -72,23 +71,9 @@
7271
<Border.StrokeShape>
7372
<RoundRectangle CornerRadius="8"/>
7473
</Border.StrokeShape>
75-
<Border.Behaviors>
76-
<behaviors:TapFeedbackBehavior/>
77-
</Border.Behaviors>
78-
<VisualStateManager.VisualStateGroups>
79-
<VisualStateGroup x:Name="CommonStates">
80-
<VisualState x:Name="Normal">
81-
<VisualState.Setters>
82-
<Setter Property="BackgroundColor" Value="Transparent"/>
83-
</VisualState.Setters>
84-
</VisualState>
85-
<VisualState x:Name="Selected">
86-
<VisualState.Setters>
87-
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
88-
</VisualState.Setters>
89-
</VisualState>
90-
</VisualStateGroup>
91-
</VisualStateManager.VisualStateGroups>
74+
<Border.GestureRecognizers>
75+
<TapGestureRecognizer Tapped="OnPlaylistTapped" CommandParameter="{Binding}"/>
76+
</Border.GestureRecognizers>
9277
<Grid ColumnDefinitions="Auto,*,Auto">
9378
<Label Text="🎵"
9479
FontSize="24"

TFMAudioApp/Controls/PlaylistPickerPopup.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ private void OnSearchTextChanged(object? sender, TextChangedEventArgs e)
4040
}
4141
}
4242

43-
private void OnPlaylistSelected(object? sender, SelectionChangedEventArgs e)
43+
private void OnPlaylistTapped(object? sender, TappedEventArgs e)
4444
{
45-
if (e.CurrentSelection.FirstOrDefault() is Playlist playlist)
45+
if (e.Parameter is Playlist playlist)
4646
{
4747
_selectedPlaylist = playlist;
4848
Result = new PlaylistPickerResult

TFMAudioApp/Services/AudioPlayerService.cs

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class AudioPlayerService : IAudioPlayerService, IDisposable
1717
private readonly Random _random = new();
1818

1919
private LibVLC? _libVLC;
20-
private MediaPlayer? _mediaPlayer;
20+
private LibVLCSharp.Shared.MediaPlayer? _mediaPlayer;
2121
private Media? _currentMedia;
2222
private System.Timers.Timer? _positionTimer;
2323
private List<Track> _originalQueue = new();
@@ -80,32 +80,25 @@ public void Initialize()
8080
// Initialize LibVLC core
8181
Core.Initialize();
8282

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

101-
// Log LibVLC messages for debugging
91+
// Only log errors from LibVLC
10292
_libVLC.Log += (s, e) =>
10393
{
104-
System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}");
94+
if (e.Level == LogLevel.Error)
95+
{
96+
System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}");
97+
}
10598
};
10699

107100
// Create media player
108-
_mediaPlayer = new MediaPlayer(_libVLC);
101+
_mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC);
109102
_mediaPlayer.Volume = (int)(_volume * 100);
110103

111104
// Setup event handlers
@@ -257,10 +250,41 @@ private void OnStopped(object? sender, EventArgs e)
257250
_positionTimer?.Stop();
258251
}
259252

253+
private bool _isSeeking;
254+
private DateTime _lastSeekTime = DateTime.MinValue;
255+
260256
private void OnEndReached(object? sender, EventArgs e)
261257
{
262258
_positionTimer?.Stop();
263259

260+
// Check if this is a false "end reached" due to a failed seek on network stream
261+
// If we were seeking recently (within 3 seconds) and position is not near the end, ignore this event
262+
var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds;
263+
if (timeSinceSeek < 3 && _mediaPlayer != null)
264+
{
265+
var currentPos = _mediaPlayer.Position;
266+
var length = _mediaPlayer.Length;
267+
var positionMs = currentPos * length;
268+
var remainingMs = length - positionMs;
269+
270+
// If more than 5 seconds remaining, this is likely a false end-of-stream from failed seek
271+
if (remainingMs > 5000)
272+
{
273+
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Ignoring false EndReached after seek (remaining: {remainingMs}ms)");
274+
// Try to resume playback
275+
MainThread.BeginInvokeOnMainThread(async () =>
276+
{
277+
await Task.Delay(500);
278+
if (_mediaPlayer != null && CurrentTrack != null)
279+
{
280+
System.Diagnostics.Debug.WriteLine("[AudioPlayer] Attempting to resume after false EndReached");
281+
await PlayAtIndexAsync(CurrentIndex);
282+
}
283+
});
284+
return;
285+
}
286+
}
287+
264288
// Handle end of track on main thread
265289
MainThread.BeginInvokeOnMainThread(async () =>
266290
{
@@ -297,7 +321,25 @@ private void OnError(object? sender, EventArgs e)
297321
}
298322

299323
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] LibVLC error occurred for track: {CurrentTrack?.FileName ?? "null"}");
300-
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}");
324+
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}, isSeeking: {_isSeeking}");
325+
326+
// If error occurred during seek on network stream, don't treat it as fatal
327+
// Just ignore the seek and continue playing from current position
328+
var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds;
329+
if (timeSinceSeek < 3)
330+
{
331+
System.Diagnostics.Debug.WriteLine("[AudioPlayer] Error during seek - attempting to resume playback");
332+
MainThread.BeginInvokeOnMainThread(async () =>
333+
{
334+
await Task.Delay(500);
335+
if (_mediaPlayer != null && CurrentTrack != null && State != PlaybackState.Playing)
336+
{
337+
// Restart the track from the beginning if we can't seek
338+
await PlayAtIndexAsync(CurrentIndex);
339+
}
340+
});
341+
return;
342+
}
301343

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

551593
public async Task SeekAsync(TimeSpan position)
552594
{
553-
if (_mediaPlayer == null || _mediaPlayer.Length <= 0) return;
595+
if (_mediaPlayer == null) return;
554596

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

558-
try
559-
{
560-
await MainThread.InvokeOnMainThreadAsync(() =>
561-
{
562-
var positionRatio = (float)(position.TotalMilliseconds / _mediaPlayer.Length);
563-
_mediaPlayer.Position = Math.Clamp(positionRatio, 0f, 1f);
564-
});
565-
}
566-
catch (Exception ex)
600+
_lastSeekTime = DateTime.UtcNow;
601+
602+
// Use Position (0.0 to 1.0 ratio) which is more reliable than Time
603+
var positionRatio = (float)(position.TotalMilliseconds / length);
604+
positionRatio = Math.Clamp(positionRatio, 0f, 0.99f);
605+
606+
await MainThread.InvokeOnMainThreadAsync(() =>
567607
{
568-
System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seek failed: {ex.Message}");
569-
// Don't propagate error - just ignore failed seek
570-
}
608+
_mediaPlayer.Position = positionRatio;
609+
});
571610
}
572611

573612
public async Task PlayAtIndexAsync(int index)

TFMAudioApp/TFMAudioApp.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@
4343
<!-- Platform minimum versions -->
4444
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
4545
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
46-
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
46+
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
4747
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
4848
</PropertyGroup>
4949

50-
<!-- Android Signing Configuration (used during Release builds) -->
50+
<!-- Android Package Format -->
5151
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
5252
<AndroidPackageFormat>apk</AndroidPackageFormat>
53+
</PropertyGroup>
54+
55+
<!-- Android Signing Configuration (only if keystore is provided) -->
56+
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release' and '$(ANDROID_KEYSTORE_PATH)' != ''">
5357
<AndroidKeyStore>true</AndroidKeyStore>
5458
<AndroidSigningKeyStore>$(ANDROID_KEYSTORE_PATH)</AndroidSigningKeyStore>
5559
<AndroidSigningKeyAlias>$(ANDROID_KEY_ALIAS)</AndroidSigningKeyAlias>

TelegramDownloader/Controllers/Mobile/MobileFileController.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,17 @@ public async Task<IActionResult> BrowseTelegramFiles(string channelId, [FromQuer
132132
items = items.Where(i => i.Name.ToLower().Contains(searchLower)).ToList();
133133
}
134134

135-
// Sort
135+
// Sort - folders always first, then apply sorting
136136
items = (request.SortBy?.ToLower(), request.SortDescending) switch
137137
{
138138
("date", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.DateModified).ToList(),
139139
("date", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.DateModified).ToList(),
140140
("size", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Size).ToList(),
141141
("size", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Size).ToList(),
142+
("name", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Name).ToList(),
143+
("name", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList(),
144+
("type", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Type).ToList(),
145+
("type", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Type).ToList(),
142146
_ => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList()
143147
};
144148

@@ -238,7 +242,16 @@ public IActionResult BrowseLocalFiles([FromQuery] BrowseRequest request)
238242
// Apply filter
239243
if (!string.IsNullOrEmpty(request.Filter) && request.Filter.ToLower() != "all")
240244
{
241-
items = items.Where(i => i.IsFolder || i.Category.ToLower() == request.Filter.ToLower()).ToList();
245+
var filterLower = request.Filter.ToLower();
246+
if (filterLower == "audio_folders")
247+
{
248+
// audio_folders = show audio files and folders
249+
items = items.Where(i => i.IsFolder || i.Category.ToLower() == "audio").ToList();
250+
}
251+
else
252+
{
253+
items = items.Where(i => i.IsFolder || i.Category.ToLower() == filterLower).ToList();
254+
}
242255
}
243256

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

251-
// Sort
264+
// Sort - folders always first, then apply sorting
252265
items = (request.SortBy?.ToLower(), request.SortDescending) switch
253266
{
254267
("date", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.DateModified).ToList(),
255268
("date", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.DateModified).ToList(),
256269
("size", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Size).ToList(),
257270
("size", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Size).ToList(),
271+
("name", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Name).ToList(),
272+
("name", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList(),
273+
("type", true) => items.OrderBy(i => !i.IsFolder).ThenByDescending(i => i.Type).ToList(),
274+
("type", false) => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Type).ToList(),
258275
_ => items.OrderBy(i => !i.IsFolder).ThenBy(i => i.Name).ToList()
259276
};
260277

0 commit comments

Comments
 (0)