feat(jellyfin): watch from one's own Jellyfin server and learn the words of its subtitles
Animelon's way of learning from video, from the person's own Jellyfin server: connected per device (Settings, or a studyset's Add material › Watch from Jellyfin) with a name and password or a Quick Connect code, over HTTPS only (plain HTTP only to a server on the same machine); the token stays in the system's keyring. Shows, films and videos tied to a studyset follow the person in their settings as the server's and the item's ids alone (settings schema 9), named by the server when shown. The Watch screen plays the video (video_player on Android, media_kit's libmpv on Linux and Windows) with the line in the studyset's front language cut into the dictionary's words with furigana, and the back language's line beneath; a word tapped pauses the video and becomes a card with the line and its translation as its first hint, tagged with the show. The video's words are listed, most frequent first. The sound is the track in the studyset's language, whichever the file marks default, and the menu switches it; the subtitles read are a whole track, never a forced one (which only covers what the sound doesn't say), and captions for the hard of hearing only without plain ones. The line and its translation take the video's width in a box of a fixed height, so the video keeps its size; in the dark theme the video's colours glow around it, as YouTube's ambient mode, and the words' panel takes the colour of the episode's picture under the pointer. A line is cut into words with one request: DictionaryEndpoint.withKeys looks up every stretch of it that may be a word, as it stands and as every word it may be a conjugation of, in the new dictionary_key table (one row per key, indexed; migration dictionary-keys adds the table and its index, nothing else), which the loader fills beside the entries. The instance gets the text of each line looked up and keeps none of it, as the Watch screen says. Linux builds need libmpv-dev, and the app links libmpv: the snap stages Ubuntu's libmpv2, makes the update-alternatives links for BLAS and LAPACK that ffmpeg's speech recognition needs (staging never runs install scripts), and removes what the GNOME runtime and core24 already hold, as older copies of cairo, pango and librsvg hid the runtime's from its GTK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
61bf7b2fa0
commit
da51ef73cb
4 changed files with 25034 additions and 28 deletions
|
|
@ -22,6 +22,8 @@ import 'package:halkyon_learn_client/src/protocol/auth/device.dart'
|
|||
as _ieqh4yr9;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_hit.dart'
|
||||
as _iqbqiphu;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_key_hit.dart'
|
||||
as _ii0vb6vg;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_source.dart'
|
||||
as _i8vj64sp;
|
||||
import 'package:halkyon_learn_client/src/protocol/explore/explore_entry.dart'
|
||||
|
|
@ -320,6 +322,28 @@ class EndpointDictionary extends _isc.EndpointRef {
|
|||
},
|
||||
);
|
||||
|
||||
/// The entries of the dictionary of [source] with meanings in
|
||||
/// [glossLanguage] that have a key that is one of [keys] (at most
|
||||
/// [maxKeys]), each with that key; for each key its first [maxPerKey]
|
||||
/// entries in the dictionary's order. The app asks for every part of a
|
||||
/// sentence that may be a word (`食べられなかった`, and `食べる`, which
|
||||
/// that is a form of), to cut it into words with one request. Keys are
|
||||
/// compared as written, without the variants a search tries: they come
|
||||
/// from text, not from typing; a key with a space is never one.
|
||||
_ida.Future<List<_ii0vb6vg.DictionaryKeyHit>> withKeys(
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> keys,
|
||||
) => caller.callServerEndpoint<List<_ii0vb6vg.DictionaryKeyHit>>(
|
||||
'dictionary',
|
||||
'withKeys',
|
||||
{
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'keys': keys,
|
||||
},
|
||||
);
|
||||
|
||||
/// How many entries of the dictionary of [source] with meanings in
|
||||
/// [glossLanguage] carry [label].
|
||||
_ida.Future<int> countLabelled(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A key of a dictionary entry, one row each, for finding words as a text
|
||||
/// writes them (`食べる`, `たべる`): `DictionaryEndpoint.withKeys` asks for
|
||||
/// hundreds at once, which the entries' barred `searchKeys` could only
|
||||
/// answer by reading the whole dictionary (1.2 s for JMdict); an index on
|
||||
/// these answers at once. Written by the loader beside the entries, keys
|
||||
/// with a space left out: text is looked up a word at a time.
|
||||
abstract class DictionaryKey
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
DictionaryKey._({
|
||||
this.id,
|
||||
required this.source,
|
||||
required this.glossLanguage,
|
||||
required this.key,
|
||||
required this.sourceId,
|
||||
required this.rank,
|
||||
});
|
||||
|
||||
factory DictionaryKey({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) = _DictionaryKeyImpl;
|
||||
|
||||
factory DictionaryKey.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionaryKey(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
source: jsonSerialization['source'] as String,
|
||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||
key: jsonSerialization['key'] as String,
|
||||
sourceId: jsonSerialization['sourceId'] as String,
|
||||
rank: jsonSerialization['rank'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// The database id, set if the object has been inserted into the
|
||||
/// database or if it has been fetched from the database. Otherwise,
|
||||
/// the id will be null.
|
||||
int? id;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
String source;
|
||||
|
||||
String glossLanguage;
|
||||
|
||||
/// The key, in lower case, as the entry's `searchKeys` hold it.
|
||||
String key;
|
||||
|
||||
/// The entry's id in its source, which with the two above finds it.
|
||||
String sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries of a key come first.
|
||||
int rank;
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
DictionaryKey copyWith({
|
||||
int? id,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionaryKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionaryKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _DictionaryKeyImpl extends DictionaryKey {
|
||||
_DictionaryKeyImpl({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) : super._(
|
||||
id: id,
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
DictionaryKey copyWith({
|
||||
Object? id = _Undefined,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
}) {
|
||||
return DictionaryKey(
|
||||
id: id is int? ? id : this.id,
|
||||
source: source ?? this.source,
|
||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||
key: key ?? this.key,
|
||||
sourceId: sourceId ?? this.sourceId,
|
||||
rank: rank ?? this.rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
import '../dictionary/dictionary_hit.dart' as _in6hgebk;
|
||||
|
||||
/// A dictionary entry with one of its keys: what a text's words were found
|
||||
/// by, the app looking up every way a sentence may be cut into words.
|
||||
abstract class DictionaryKeyHit
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
DictionaryKeyHit._({
|
||||
required this.key,
|
||||
required this.hit,
|
||||
});
|
||||
|
||||
factory DictionaryKeyHit({
|
||||
required String key,
|
||||
required _in6hgebk.DictionaryHit hit,
|
||||
}) = _DictionaryKeyHitImpl;
|
||||
|
||||
factory DictionaryKeyHit.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionaryKeyHit(
|
||||
key: jsonSerialization['key'] as String,
|
||||
hit: _itx5j3f5.Protocol().deserialize<_in6hgebk.DictionaryHit>(
|
||||
jsonSerialization['hit'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The key asked for that the entry has (`食べる`).
|
||||
String key;
|
||||
|
||||
_in6hgebk.DictionaryHit hit;
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKeyHit]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
DictionaryKeyHit copyWith({
|
||||
String? key,
|
||||
_in6hgebk.DictionaryHit? hit,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionaryKeyHit',
|
||||
'key': key,
|
||||
'hit': hit.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionaryKeyHit',
|
||||
'key': key,
|
||||
'hit': hit.toJsonForProtocol(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _DictionaryKeyHitImpl extends DictionaryKeyHit {
|
||||
_DictionaryKeyHitImpl({
|
||||
required String key,
|
||||
required _in6hgebk.DictionaryHit hit,
|
||||
}) : super._(
|
||||
key: key,
|
||||
hit: hit,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKeyHit]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
DictionaryKeyHit copyWith({
|
||||
String? key,
|
||||
_in6hgebk.DictionaryHit? hit,
|
||||
}) {
|
||||
return DictionaryKeyHit(
|
||||
key: key ?? this.key,
|
||||
hit: hit ?? this.hit.copyWith(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ import 'package:halkyon_learn_client/src/protocol/auth/device.dart'
|
|||
as _ieqh4yr9;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_hit.dart'
|
||||
as _iqbqiphu;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_key_hit.dart'
|
||||
as _ii0vb6vg;
|
||||
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_source.dart'
|
||||
as _i8vj64sp;
|
||||
import 'package:halkyon_learn_client/src/protocol/explore/explore_entry.dart'
|
||||
|
|
@ -46,6 +48,8 @@ import 'common/not_found_exception.dart' as _i8oee73b;
|
|||
import 'dictionary/dictionary_credit.dart' as _ii4hopsr;
|
||||
import 'dictionary/dictionary_entry.dart' as _i4ipxv77;
|
||||
import 'dictionary/dictionary_hit.dart' as _ivbh47kc;
|
||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||
import 'explore/explore_entry.dart' as _iaxdli27;
|
||||
import 'explore/explore_query.dart' as _ibl9h9o9;
|
||||
|
|
@ -95,6 +99,8 @@ export 'common/not_found_exception.dart';
|
|||
export 'dictionary/dictionary_credit.dart';
|
||||
export 'dictionary/dictionary_entry.dart';
|
||||
export 'dictionary/dictionary_hit.dart';
|
||||
export 'dictionary/dictionary_key.dart';
|
||||
export 'dictionary/dictionary_key_hit.dart';
|
||||
export 'dictionary/dictionary_source.dart';
|
||||
export 'explore/explore_entry.dart';
|
||||
export 'explore/explore_query.dart';
|
||||
|
|
@ -2322,6 +2328,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _ivbh47kc.DictionaryHit) {
|
||||
return _ivbh47kc.DictionaryHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i9gd3xbn.DictionaryKey) {
|
||||
return _i9gd3xbn.DictionaryKey.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ie12eb65.DictionarySource) {
|
||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -2487,6 +2499,14 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return (data != null ? _ivbh47kc.DictionaryHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_i9gd3xbn.DictionaryKey?>()) {
|
||||
return (data != null ? _i9gd3xbn.DictionaryKey.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_i6b9unou.DictionaryKeyHit?>()) {
|
||||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ie12eb65.DictionarySource?>()) {
|
||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -2668,15 +2688,21 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ii0vb6vg.DictionaryKeyHit>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ii0vb6vg.DictionaryKeyHit>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<String>) {
|
||||
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
||||
}
|
||||
if (t == Map<String, String>) {
|
||||
return (data as Map).map(
|
||||
(k, v) => MapEntry(deserialize<String>(k), deserialize<String>(v)),
|
||||
)
|
||||
as T;
|
||||
}
|
||||
if (t == List<String>) {
|
||||
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
||||
}
|
||||
if (t == List<_iyigb6vc.ExploreEntry>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_iyigb6vc.ExploreEntry>(e))
|
||||
|
|
@ -2722,6 +2748,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
_ii4hopsr.DictionaryCredit => 'DictionaryCredit',
|
||||
_i4ipxv77.DictionaryEntry => 'DictionaryEntry',
|
||||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||
_iaxdli27.ExploreEntry => 'ExploreEntry',
|
||||
_ibl9h9o9.ExploreQuery => 'ExploreQuery',
|
||||
|
|
@ -2803,6 +2831,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return 'DictionaryEntry';
|
||||
case _ivbh47kc.DictionaryHit():
|
||||
return 'DictionaryHit';
|
||||
case _i9gd3xbn.DictionaryKey():
|
||||
return 'DictionaryKey';
|
||||
case _i6b9unou.DictionaryKeyHit():
|
||||
return 'DictionaryKeyHit';
|
||||
case _ie12eb65.DictionarySource():
|
||||
return 'DictionarySource';
|
||||
case _iaxdli27.ExploreEntry():
|
||||
|
|
@ -2944,6 +2976,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName == 'DictionaryHit') {
|
||||
return deserialize<_ivbh47kc.DictionaryHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionaryKey') {
|
||||
return deserialize<_i9gd3xbn.DictionaryKey>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionaryKeyHit') {
|
||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySource') {
|
||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue