fix(train): Jellyfin over https alone, but on the phone itself (ADR 0030, D54)

An http address to another machine is refused as typed, saying why: over it the password, the token and what the person listens to went through the network in clear, and the music job, held by Android to https, retried for ever without a track. A Jellyfin on the phone (localhost, 127.0.0.1) stays reachable over http, the network security config allowing those two alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-30 12:55:59 +01:00
commit ced82b1e75
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
8 changed files with 74 additions and 0 deletions

View file

@ -481,6 +481,7 @@
"musicServer": "Jellyfin-Server",
"musicConnect": "Verbinden",
"musicBadAddress": "Gib die Adresse des Servers ein, etwa jellyfin.example.org.",
"musicInClear": "Train verbindet sich mit Jellyfin nur über https: Über http ginge das Passwort unverschlüsselt durchs Netz. Gib die https-Adresse des Servers ein.",
"musicUnreachable": "Der Server ist nicht erreichbar. Prüfe die Adresse und die Verbindung.",
"musicNotJellyfin": "Unter dieser Adresse ist kein Jellyfin-Server.",
"musicRefused": "Jellyfin hat abgelehnt. Prüfe Benutzername und Passwort.",

View file

@ -607,6 +607,7 @@
"@musicServer": {"description": "The label of the server's address."},
"musicConnect": "Connect",
"musicBadAddress": "Enter the server's address, such as jellyfin.example.org.",
"musicInClear": "Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server's https address.",
"musicUnreachable": "The server can't be reached. Check the address and the connection.",
"musicNotJellyfin": "There is no Jellyfin server at this address.",
"musicRefused": "Jellyfin refused. Check the user name and password.",

View file

@ -2908,6 +2908,12 @@ abstract class AppLocalizations {
/// **'Enter the server\'s address, such as jellyfin.example.org.'**
String get musicBadAddress;
/// No description provided for @musicInClear.
///
/// In en, this message translates to:
/// **'Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server\'s https address.'**
String get musicInClear;
/// No description provided for @musicUnreachable.
///
/// In en, this message translates to:

View file

@ -1723,6 +1723,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get musicBadAddress =>
'Gib die Adresse des Servers ein, etwa jellyfin.example.org.';
@override
String get musicInClear =>
'Train verbindet sich mit Jellyfin nur über https: Über http ginge das Passwort unverschlüsselt durchs Netz. Gib die https-Adresse des Servers ein.';
@override
String get musicUnreachable =>
'Der Server ist nicht erreichbar. Prüfe die Adresse und die Verbindung.';

View file

@ -1713,6 +1713,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get musicBadAddress =>
'Enter the server\'s address, such as jellyfin.example.org.';
@override
String get musicInClear =>
'Train connects to Jellyfin over https only: http would send the password unencrypted. Use the server\'s https address.';
@override
String get musicUnreachable =>
'The server can\'t be reached. Check the address and the connection.';

View file

@ -35,6 +35,9 @@ enum MusicStep() {
/// Train's own.
enum MusicFailure() {
badAddress,
/// http to another machine than the phone (D47).
inClear,
unreachable,
notJellyfin,
refused,
@ -126,6 +129,10 @@ class MusicCubit(
emit(state.copyWith(failure: () => MusicFailure.badAddress));
return;
}
if (jellyfinInClear(address)) {
emit(state.copyWith(failure: () => MusicFailure.inClear));
return;
}
emit(state.copyWith(step: MusicStep.finding, failure: () => null));
try {
final server = await _library.find(address);

View file

@ -86,6 +86,14 @@ Uri? jellyfinAddress(String typed) {
);
}
/// Whether [address] would carry the password and the token in clear: http
/// to anywhere but this phone itself (D47). Train asks Jellyfin over https
/// alone, but for a server on the phone (`localhost`, `127.0.0.1`), which
/// the app's network security config lets the downloads reach too.
bool jellyfinInClear(Uri address) =>
address.scheme == 'http' &&
!const {'localhost', '127.0.0.1'}.contains(address.host.toLowerCase());
/// Jellyfin's API as Train asks it (D47), over plain HTTP: as the client
/// "Halkyon Train" on the device `deviceId`, to which Jellyfin ties the
/// tokens it gives.

View file

@ -63,6 +63,7 @@ class const MusicScreen({
String _failureText(AppLocalizations l10n, MusicFailure failure) =>
switch (failure) {
MusicFailure.badAddress => l10n.musicBadAddress,
MusicFailure.inClear => l10n.musicInClear,
MusicFailure.unreachable => l10n.musicUnreachable,
MusicFailure.notJellyfin => l10n.musicNotJellyfin,
MusicFailure.refused => l10n.musicRefused,