feat(jellyfin): a show's tags follow it whatever it is called
A word met on Jellyfin is tagged with the show and the episode by the names Jellyfin gives them, so a server switching its metadata language, another server, or a rename in Manage tags made a second set of tags for the same show. Such a tag now also keeps what it stands for: the ids the show (or the artist, or the film) has with the metadata providers and on the server, and the episode's number or the song's own id. New words find their tag by those first, keeping its name; a Jellyfin tag from before, found by its name, takes them, and a rename keeps them. They stay in tag schema 2, a deliberate exception to ADR 0002's rule 3: schema 3 would hide every new Jellyfin tag from v0.17, while an app that rewrites the tag only drops the ids, and the tag is found by its name again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
838f753bf2
commit
238a1a93ae
1 changed files with 619 additions and 77 deletions
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway
|
|||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
|
|
@ -78,6 +79,10 @@ MockClient jellyfin12(List<http.Request> requests) => MockClient((
|
|||
'Name': '幽霊船戦',
|
||||
'Id': 'song-1',
|
||||
'Type': 'Audio',
|
||||
'AlbumArtists': [
|
||||
{'Name': '宝鐘マリン', 'Id': 'artist-1'},
|
||||
],
|
||||
'ProviderIds': {'MusicBrainzTrack': 'mb-track-1'},
|
||||
'AlbumArtist': '宝鐘マリン',
|
||||
'Album': 'Ahoy!!',
|
||||
'HasLyrics': true,
|
||||
|
|
@ -98,6 +103,8 @@ MockClient jellyfin12(List<http.Request> requests) => MockClient((
|
|||
'Id': 'ep-1',
|
||||
'Type': 'Episode',
|
||||
'SeriesName': 'Sample Journey',
|
||||
'SeriesId': 'series-1',
|
||||
'ProviderIds': {'Tmdb': '9001', 'Tvdb': ''},
|
||||
'IndexNumber': 1,
|
||||
'ParentIndexNumber': 1,
|
||||
'RunTimeTicks': 520230000,
|
||||
|
|
@ -183,6 +190,12 @@ MockClient jellyfin12(List<http.Request> requests) => MockClient((
|
|||
utf8.encode('WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n今日は\n'),
|
||||
200,
|
||||
),
|
||||
('GET', '/Items/series-1') => json({
|
||||
'Name': 'Sample Journey',
|
||||
'Id': 'series-1',
|
||||
'Type': 'Series',
|
||||
'providerIds': {'Tmdb': '219109', 'Tvdb': '445566'},
|
||||
}),
|
||||
('POST', '/Sessions/Playing/Stopped') => json(null, 204),
|
||||
_ => json(null, 404),
|
||||
};
|
||||
|
|
@ -336,6 +349,22 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('an item says what the metadata providers call it, and an episode '
|
||||
'its show, a song its artist', () async {
|
||||
final account = await signIn();
|
||||
|
||||
final episode = await gateway.item(account, 'ep-1');
|
||||
expect(episode.providerIds, {'tmdb': '9001'});
|
||||
expect(episode.seriesId, 'series-1');
|
||||
expect((await gateway.item(account, 'series-1')).providerIds, {
|
||||
'tmdb': '219109',
|
||||
'tvdb': '445566',
|
||||
});
|
||||
final song = await gateway.item(account, 'song-1');
|
||||
expect(song.artistId, 'artist-1');
|
||||
expect(song.providerIds, {'musicbrainztrack': 'mb-track-1'});
|
||||
});
|
||||
|
||||
test('a song is one, with its artist and whether it has lyrics, its '
|
||||
'lyrics timed line by line, none for a song without; a song streams '
|
||||
'as sound', () async {
|
||||
|
|
@ -673,6 +702,109 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('the tags a word met on Jellyfin gets', () {
|
||||
const series = JellyfinItem(
|
||||
id: 'series-1',
|
||||
name: 'あかね噺',
|
||||
kind: JellyfinItemKind.series,
|
||||
providerIds: {'tmdb': '219109'},
|
||||
);
|
||||
const episode = JellyfinItem(
|
||||
id: 'ep-1',
|
||||
name: '第1話',
|
||||
kind: JellyfinItemKind.episode,
|
||||
seriesName: 'あかね噺',
|
||||
seriesId: 'series-1',
|
||||
seasonNumber: 1,
|
||||
episodeNumber: 1,
|
||||
providerIds: {'tmdb': '9001'},
|
||||
);
|
||||
const show = JellyfinTagId(
|
||||
ids: {'tmdb': '219109', 'jellyfin:server-1': 'series-1'},
|
||||
);
|
||||
|
||||
test("an episode: its show, by the show's ids, and itself, by them and "
|
||||
'its number', () {
|
||||
expect(jellyfinTagsOf(episode, serverId: 'server-1', whole: series), (
|
||||
whole: (name: 'あかね噺', id: show),
|
||||
part: (
|
||||
name: 'あかね噺 S1 E1',
|
||||
id: JellyfinTagId(ids: show.ids, part: 'S1E1'),
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
test('without its show read, the show is known by its id on the '
|
||||
'server', () {
|
||||
expect(
|
||||
jellyfinTagsOf(episode, serverId: 'server-1').whole.id,
|
||||
const JellyfinTagId(ids: {'jellyfin:server-1': 'series-1'}),
|
||||
);
|
||||
});
|
||||
|
||||
test('a film is one tag, by its own ids', () {
|
||||
expect(
|
||||
jellyfinTagsOf(
|
||||
const JellyfinItem(
|
||||
id: 'film-1',
|
||||
name: 'CODA',
|
||||
kind: JellyfinItemKind.movie,
|
||||
providerIds: {'tmdb': '776503', 'imdb': 'tt10366460'},
|
||||
),
|
||||
serverId: 'server-1',
|
||||
),
|
||||
(
|
||||
whole: (
|
||||
name: 'CODA',
|
||||
id: const JellyfinTagId(
|
||||
ids: {
|
||||
'tmdb': '776503',
|
||||
'imdb': 'tt10366460',
|
||||
'jellyfin:server-1': 'film-1',
|
||||
},
|
||||
),
|
||||
),
|
||||
part: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("a song: its artist, by the artist's ids, and itself, by its "
|
||||
'own', () {
|
||||
const song = JellyfinItem(
|
||||
id: 'song-1',
|
||||
name: 'Ahoy!!',
|
||||
kind: JellyfinItemKind.audio,
|
||||
artist: '宝鐘マリン',
|
||||
artistId: 'artist-1',
|
||||
providerIds: {'musicbrainztrack': 'mb-track-1'},
|
||||
);
|
||||
const marine = JellyfinItem(
|
||||
id: 'artist-1',
|
||||
name: '宝鐘マリン',
|
||||
kind: JellyfinItemKind.artist,
|
||||
providerIds: {'musicbrainzartist': 'mb-artist-1'},
|
||||
);
|
||||
const artist = JellyfinTagId(
|
||||
ids: {
|
||||
'musicbrainzartist': 'mb-artist-1',
|
||||
'jellyfin:server-1': 'artist-1',
|
||||
},
|
||||
);
|
||||
|
||||
expect(jellyfinTagsOf(song, serverId: 'server-1', whole: marine), (
|
||||
whole: (name: '宝鐘マリン', id: artist),
|
||||
part: (
|
||||
name: '宝鐘マリン · Ahoy!!',
|
||||
id: JellyfinTagId(
|
||||
ids: artist.ids,
|
||||
part: 'musicbrainztrack:mb-track-1',
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
test("a studyset's language finds Jellyfin's", () {
|
||||
expect(jellyfinLanguagesOf('ja'), {'jpn'});
|
||||
expect(jellyfinLanguagesOf('de-AT'), {'ger', 'deu'});
|
||||
|
|
|
|||
Loading…
Reference in a new issue