From 99932127fc1442038f8da8f91ebb58c09206a064 Mon Sep 17 00:00:00 2001 From: uday chauhan Date: Sun, 19 Jul 2026 07:46:25 +0530 Subject: [PATCH] feat(platform_playlist): CV-017 -- persistent canonical channel identity (Drift/SQLite) Adds the last remaining piece of #821's framework scope: real, queryable persistence for canonical channel identities and provider aliases, backed by Drift/SQLite instead of the SharedPreferences-JSON pattern every other store in this repo uses. Schema (matches the issue's suggested data contract): - CanonicalChannels: canonicalChannelId, displayName, normalizedName, language, country, category, logoFingerprint, createdAt, updatedAt. - ProviderChannelAliases: sourceId + providerChannelId (composite PK), canonicalChannelId (nullable FK), providerName, normalizedProviderName, tvgId, groupTitle, streamUrlFingerprint, resolution, isVod/isRadio/isAdult, matchConfidence. CanonicalChannelRepository wraps the generated Drift database: upsert/get/list for canonical channels, upsert/lookup for aliases (by composite key, by canonical id, by tvg-id -- the last one mirrors CanonicalChannelMatcher's own high-confidence signal). ## Why Drift/SQLite over the existing SharedPreferences pattern Flat JSON-in-a-single-preference-key doesn't hold up at the scale this issue describes ("tens of thousands of channels") -- no indexing, no querying, and SharedPreferences has a hard per-value size ceiling this codebase has already hit in other stores' tests. A real SQL engine with indexes on canonical_channel_id and tvg_id is the right tool once alias counts get large. ## First-of-its-kind dependency -- flags for review This is the first Drift/SQLite usage anywhere in packages/. Two things a reviewer should weigh in on: 1. New dependency footprint: drift, drift_dev, build_runner, sqlite3, sqlite3_flutter_libs, path, path_provider -- binary size and build time impact on TV specifically should get a look (chief-performance- officer) alongside the usual license/maintenance checks (chief-open- source-officer). 2. Generated code (*.g.dart) is committed despite the repo-wide .gitignore rule (force-added) because no CI workflow currently runs `dart run build_runner build` for any package under packages/ -- only app/ has that step. Without checking in the generated file, this package would fail to compile anywhere that doesn't run codegen first. The real fix is wiring a per-package (or matrix) codegen step into CI; committing the artifact is a stopgap, not a long-term policy for future Drift schema changes in this package. ## Not in this slice - Wiring FavoriteChannelRemapper/CanonicalChannelMatcher to actually populate this database during a real playlist import -- this PR only adds the storage layer. - Migration strategy beyond schemaVersion 1 (no prior schema to migrate from yet). --- .../lib/platform_playlist.dart | 2 + .../canonical_channel_database.dart | 78 + .../canonical_channel_database.g.dart | 2133 +++++++++++++++++ .../canonical_channel_repository.dart | 61 + packages/platform_playlist/pubspec.yaml | 7 + .../canonical_channel_repository_test.dart | 166 ++ 6 files changed, 2447 insertions(+) create mode 100644 packages/platform_playlist/lib/src/persistence/canonical_channel_database.dart create mode 100644 packages/platform_playlist/lib/src/persistence/canonical_channel_database.g.dart create mode 100644 packages/platform_playlist/lib/src/persistence/canonical_channel_repository.dart create mode 100644 packages/platform_playlist/test/canonical_channel_repository_test.dart diff --git a/packages/platform_playlist/lib/platform_playlist.dart b/packages/platform_playlist/lib/platform_playlist.dart index 71b0d3ef..de3f4b37 100644 --- a/packages/platform_playlist/lib/platform_playlist.dart +++ b/packages/platform_playlist/lib/platform_playlist.dart @@ -15,6 +15,8 @@ export 'src/smart_playlist_evaluator.dart'; export 'src/channel_name_normalizer.dart'; export 'src/canonical_channel_matcher.dart'; export 'src/favorite_channel_remapper.dart'; +export 'src/persistence/canonical_channel_database.dart'; +export 'src/persistence/canonical_channel_repository.dart'; export 'src/xtream/xtream_client.dart'; export 'src/xtream/xtream_content_source.dart'; export 'src/xtream/xtream_epg_repository.dart'; diff --git a/packages/platform_playlist/lib/src/persistence/canonical_channel_database.dart b/packages/platform_playlist/lib/src/persistence/canonical_channel_database.dart new file mode 100644 index 00000000..fa2a01bc --- /dev/null +++ b/packages/platform_playlist/lib/src/persistence/canonical_channel_database.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +part 'canonical_channel_database.g.dart'; + +/// Canonical channel identities (CV-017): a normalized, user-facing channel +/// concept that provider aliases point to when confidently matched. +class CanonicalChannels extends Table { + TextColumn get canonicalChannelId => text()(); + TextColumn get displayName => text()(); + TextColumn get normalizedName => text()(); + TextColumn get language => text().nullable()(); + TextColumn get country => text().nullable()(); + TextColumn get category => text().nullable()(); + TextColumn get logoFingerprint => text().nullable()(); + DateTimeColumn get createdAt => dateTime()(); + DateTimeColumn get updatedAt => dateTime()(); + + @override + Set get primaryKey => {canonicalChannelId}; +} + +/// A single provider's channel entry, optionally pointing at a canonical +/// identity once matched with sufficient confidence (CV-017). Kept separate +/// from [CanonicalChannels] per the issue's "store provider channel aliases +/// separately from user-facing canonical identities" framework rule. +@DataClassName('ProviderChannelAlias') +class ProviderChannelAliases extends Table { + TextColumn get sourceId => text()(); + TextColumn get providerChannelId => text()(); + TextColumn get canonicalChannelId => text().nullable()(); + TextColumn get providerName => text()(); + TextColumn get normalizedProviderName => text()(); + IntColumn get tvgId => integer().nullable()(); + TextColumn get groupTitle => text().nullable()(); + TextColumn get streamUrlFingerprint => text()(); + TextColumn get resolution => text().nullable()(); + BoolColumn get isVod => boolean().withDefault(const Constant(false))(); + BoolColumn get isRadio => boolean().withDefault(const Constant(false))(); + BoolColumn get isAdult => boolean().withDefault(const Constant(false))(); + // Stored as the ChannelMatchConfidence enum's name ('high'/'medium'/ + // 'low'/'none'), not a raw double -- keeps the persisted value tied to + // CanonicalChannelMatcher's actual confidence tiers instead of an + // arbitrary score that could drift out of sync with matcher logic. + TextColumn get matchConfidence => text().nullable()(); + + @override + Set get primaryKey => {sourceId, providerChannelId}; +} + +@DriftDatabase(tables: [CanonicalChannels, ProviderChannelAliases]) +class CanonicalChannelDatabase extends _$CanonicalChannelDatabase { + CanonicalChannelDatabase(super.executor); + + /// Opens the real on-device database file. Not used in tests -- see + /// [CanonicalChannelDatabase.forTesting] for an in-memory instance. + factory CanonicalChannelDatabase.open() { + return CanonicalChannelDatabase( + LazyDatabase(() async { + final dir = await getApplicationSupportDirectory(); + final file = File(p.join(dir.path, 'canonical_channels.sqlite')); + return NativeDatabase.createInBackground(file); + }), + ); + } + + /// An in-memory database for tests -- never touches disk. + factory CanonicalChannelDatabase.forTesting() { + return CanonicalChannelDatabase(NativeDatabase.memory()); + } + + @override + int get schemaVersion => 1; +} diff --git a/packages/platform_playlist/lib/src/persistence/canonical_channel_database.g.dart b/packages/platform_playlist/lib/src/persistence/canonical_channel_database.g.dart new file mode 100644 index 00000000..826467a0 --- /dev/null +++ b/packages/platform_playlist/lib/src/persistence/canonical_channel_database.g.dart @@ -0,0 +1,2133 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'canonical_channel_database.dart'; + +// ignore_for_file: type=lint +class $CanonicalChannelsTable extends CanonicalChannels + with TableInfo<$CanonicalChannelsTable, CanonicalChannel> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CanonicalChannelsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _canonicalChannelIdMeta = + const VerificationMeta('canonicalChannelId'); + @override + late final GeneratedColumn canonicalChannelId = + GeneratedColumn( + 'canonical_channel_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _normalizedNameMeta = const VerificationMeta( + 'normalizedName', + ); + @override + late final GeneratedColumn normalizedName = GeneratedColumn( + 'normalized_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _languageMeta = const VerificationMeta( + 'language', + ); + @override + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _countryMeta = const VerificationMeta( + 'country', + ); + @override + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _categoryMeta = const VerificationMeta( + 'category', + ); + @override + late final GeneratedColumn category = GeneratedColumn( + 'category', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _logoFingerprintMeta = const VerificationMeta( + 'logoFingerprint', + ); + @override + late final GeneratedColumn logoFingerprint = GeneratedColumn( + 'logo_fingerprint', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + canonicalChannelId, + displayName, + normalizedName, + language, + country, + category, + logoFingerprint, + createdAt, + updatedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'canonical_channels'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('canonical_channel_id')) { + context.handle( + _canonicalChannelIdMeta, + canonicalChannelId.isAcceptableOrUnknown( + data['canonical_channel_id']!, + _canonicalChannelIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_canonicalChannelIdMeta); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_displayNameMeta); + } + if (data.containsKey('normalized_name')) { + context.handle( + _normalizedNameMeta, + normalizedName.isAcceptableOrUnknown( + data['normalized_name']!, + _normalizedNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_normalizedNameMeta); + } + if (data.containsKey('language')) { + context.handle( + _languageMeta, + language.isAcceptableOrUnknown(data['language']!, _languageMeta), + ); + } + if (data.containsKey('country')) { + context.handle( + _countryMeta, + country.isAcceptableOrUnknown(data['country']!, _countryMeta), + ); + } + if (data.containsKey('category')) { + context.handle( + _categoryMeta, + category.isAcceptableOrUnknown(data['category']!, _categoryMeta), + ); + } + if (data.containsKey('logo_fingerprint')) { + context.handle( + _logoFingerprintMeta, + logoFingerprint.isAcceptableOrUnknown( + data['logo_fingerprint']!, + _logoFingerprintMeta, + ), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {canonicalChannelId}; + @override + CanonicalChannel map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CanonicalChannel( + canonicalChannelId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}canonical_channel_id'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + )!, + normalizedName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}normalized_name'], + )!, + language: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}language'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + category: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}category'], + ), + logoFingerprint: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}logo_fingerprint'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + $CanonicalChannelsTable createAlias(String alias) { + return $CanonicalChannelsTable(attachedDatabase, alias); + } +} + +class CanonicalChannel extends DataClass + implements Insertable { + final String canonicalChannelId; + final String displayName; + final String normalizedName; + final String? language; + final String? country; + final String? category; + final String? logoFingerprint; + final DateTime createdAt; + final DateTime updatedAt; + const CanonicalChannel({ + required this.canonicalChannelId, + required this.displayName, + required this.normalizedName, + this.language, + this.country, + this.category, + this.logoFingerprint, + required this.createdAt, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['canonical_channel_id'] = Variable(canonicalChannelId); + map['display_name'] = Variable(displayName); + map['normalized_name'] = Variable(normalizedName); + if (!nullToAbsent || language != null) { + map['language'] = Variable(language); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || category != null) { + map['category'] = Variable(category); + } + if (!nullToAbsent || logoFingerprint != null) { + map['logo_fingerprint'] = Variable(logoFingerprint); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } + + CanonicalChannelsCompanion toCompanion(bool nullToAbsent) { + return CanonicalChannelsCompanion( + canonicalChannelId: Value(canonicalChannelId), + displayName: Value(displayName), + normalizedName: Value(normalizedName), + language: language == null && nullToAbsent + ? const Value.absent() + : Value(language), + country: country == null && nullToAbsent + ? const Value.absent() + : Value(country), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + logoFingerprint: logoFingerprint == null && nullToAbsent + ? const Value.absent() + : Value(logoFingerprint), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + ); + } + + factory CanonicalChannel.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CanonicalChannel( + canonicalChannelId: serializer.fromJson( + json['canonicalChannelId'], + ), + displayName: serializer.fromJson(json['displayName']), + normalizedName: serializer.fromJson(json['normalizedName']), + language: serializer.fromJson(json['language']), + country: serializer.fromJson(json['country']), + category: serializer.fromJson(json['category']), + logoFingerprint: serializer.fromJson(json['logoFingerprint']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'canonicalChannelId': serializer.toJson(canonicalChannelId), + 'displayName': serializer.toJson(displayName), + 'normalizedName': serializer.toJson(normalizedName), + 'language': serializer.toJson(language), + 'country': serializer.toJson(country), + 'category': serializer.toJson(category), + 'logoFingerprint': serializer.toJson(logoFingerprint), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + CanonicalChannel copyWith({ + String? canonicalChannelId, + String? displayName, + String? normalizedName, + Value language = const Value.absent(), + Value country = const Value.absent(), + Value category = const Value.absent(), + Value logoFingerprint = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + }) => CanonicalChannel( + canonicalChannelId: canonicalChannelId ?? this.canonicalChannelId, + displayName: displayName ?? this.displayName, + normalizedName: normalizedName ?? this.normalizedName, + language: language.present ? language.value : this.language, + country: country.present ? country.value : this.country, + category: category.present ? category.value : this.category, + logoFingerprint: logoFingerprint.present + ? logoFingerprint.value + : this.logoFingerprint, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + CanonicalChannel copyWithCompanion(CanonicalChannelsCompanion data) { + return CanonicalChannel( + canonicalChannelId: data.canonicalChannelId.present + ? data.canonicalChannelId.value + : this.canonicalChannelId, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + normalizedName: data.normalizedName.present + ? data.normalizedName.value + : this.normalizedName, + language: data.language.present ? data.language.value : this.language, + country: data.country.present ? data.country.value : this.country, + category: data.category.present ? data.category.value : this.category, + logoFingerprint: data.logoFingerprint.present + ? data.logoFingerprint.value + : this.logoFingerprint, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('CanonicalChannel(') + ..write('canonicalChannelId: $canonicalChannelId, ') + ..write('displayName: $displayName, ') + ..write('normalizedName: $normalizedName, ') + ..write('language: $language, ') + ..write('country: $country, ') + ..write('category: $category, ') + ..write('logoFingerprint: $logoFingerprint, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + canonicalChannelId, + displayName, + normalizedName, + language, + country, + category, + logoFingerprint, + createdAt, + updatedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CanonicalChannel && + other.canonicalChannelId == this.canonicalChannelId && + other.displayName == this.displayName && + other.normalizedName == this.normalizedName && + other.language == this.language && + other.country == this.country && + other.category == this.category && + other.logoFingerprint == this.logoFingerprint && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class CanonicalChannelsCompanion extends UpdateCompanion { + final Value canonicalChannelId; + final Value displayName; + final Value normalizedName; + final Value language; + final Value country; + final Value category; + final Value logoFingerprint; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const CanonicalChannelsCompanion({ + this.canonicalChannelId = const Value.absent(), + this.displayName = const Value.absent(), + this.normalizedName = const Value.absent(), + this.language = const Value.absent(), + this.country = const Value.absent(), + this.category = const Value.absent(), + this.logoFingerprint = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CanonicalChannelsCompanion.insert({ + required String canonicalChannelId, + required String displayName, + required String normalizedName, + this.language = const Value.absent(), + this.country = const Value.absent(), + this.category = const Value.absent(), + this.logoFingerprint = const Value.absent(), + required DateTime createdAt, + required DateTime updatedAt, + this.rowid = const Value.absent(), + }) : canonicalChannelId = Value(canonicalChannelId), + displayName = Value(displayName), + normalizedName = Value(normalizedName), + createdAt = Value(createdAt), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? canonicalChannelId, + Expression? displayName, + Expression? normalizedName, + Expression? language, + Expression? country, + Expression? category, + Expression? logoFingerprint, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (canonicalChannelId != null) + 'canonical_channel_id': canonicalChannelId, + if (displayName != null) 'display_name': displayName, + if (normalizedName != null) 'normalized_name': normalizedName, + if (language != null) 'language': language, + if (country != null) 'country': country, + if (category != null) 'category': category, + if (logoFingerprint != null) 'logo_fingerprint': logoFingerprint, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CanonicalChannelsCompanion copyWith({ + Value? canonicalChannelId, + Value? displayName, + Value? normalizedName, + Value? language, + Value? country, + Value? category, + Value? logoFingerprint, + Value? createdAt, + Value? updatedAt, + Value? rowid, + }) { + return CanonicalChannelsCompanion( + canonicalChannelId: canonicalChannelId ?? this.canonicalChannelId, + displayName: displayName ?? this.displayName, + normalizedName: normalizedName ?? this.normalizedName, + language: language ?? this.language, + country: country ?? this.country, + category: category ?? this.category, + logoFingerprint: logoFingerprint ?? this.logoFingerprint, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (canonicalChannelId.present) { + map['canonical_channel_id'] = Variable(canonicalChannelId.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (normalizedName.present) { + map['normalized_name'] = Variable(normalizedName.value); + } + if (language.present) { + map['language'] = Variable(language.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + if (logoFingerprint.present) { + map['logo_fingerprint'] = Variable(logoFingerprint.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CanonicalChannelsCompanion(') + ..write('canonicalChannelId: $canonicalChannelId, ') + ..write('displayName: $displayName, ') + ..write('normalizedName: $normalizedName, ') + ..write('language: $language, ') + ..write('country: $country, ') + ..write('category: $category, ') + ..write('logoFingerprint: $logoFingerprint, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ProviderChannelAliasesTable extends ProviderChannelAliases + with TableInfo<$ProviderChannelAliasesTable, ProviderChannelAlias> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProviderChannelAliasesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _sourceIdMeta = const VerificationMeta( + 'sourceId', + ); + @override + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _providerChannelIdMeta = const VerificationMeta( + 'providerChannelId', + ); + @override + late final GeneratedColumn providerChannelId = + GeneratedColumn( + 'provider_channel_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _canonicalChannelIdMeta = + const VerificationMeta('canonicalChannelId'); + @override + late final GeneratedColumn canonicalChannelId = + GeneratedColumn( + 'canonical_channel_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _providerNameMeta = const VerificationMeta( + 'providerName', + ); + @override + late final GeneratedColumn providerName = GeneratedColumn( + 'provider_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _normalizedProviderNameMeta = + const VerificationMeta('normalizedProviderName'); + @override + late final GeneratedColumn normalizedProviderName = + GeneratedColumn( + 'normalized_provider_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _tvgIdMeta = const VerificationMeta('tvgId'); + @override + late final GeneratedColumn tvgId = GeneratedColumn( + 'tvg_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _groupTitleMeta = const VerificationMeta( + 'groupTitle', + ); + @override + late final GeneratedColumn groupTitle = GeneratedColumn( + 'group_title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _streamUrlFingerprintMeta = + const VerificationMeta('streamUrlFingerprint'); + @override + late final GeneratedColumn streamUrlFingerprint = + GeneratedColumn( + 'stream_url_fingerprint', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _resolutionMeta = const VerificationMeta( + 'resolution', + ); + @override + late final GeneratedColumn resolution = GeneratedColumn( + 'resolution', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isVodMeta = const VerificationMeta('isVod'); + @override + late final GeneratedColumn isVod = GeneratedColumn( + 'is_vod', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_vod" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _isRadioMeta = const VerificationMeta( + 'isRadio', + ); + @override + late final GeneratedColumn isRadio = GeneratedColumn( + 'is_radio', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_radio" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _isAdultMeta = const VerificationMeta( + 'isAdult', + ); + @override + late final GeneratedColumn isAdult = GeneratedColumn( + 'is_adult', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_adult" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _matchConfidenceMeta = const VerificationMeta( + 'matchConfidence', + ); + @override + late final GeneratedColumn matchConfidence = GeneratedColumn( + 'match_confidence', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + sourceId, + providerChannelId, + canonicalChannelId, + providerName, + normalizedProviderName, + tvgId, + groupTitle, + streamUrlFingerprint, + resolution, + isVod, + isRadio, + isAdult, + matchConfidence, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'provider_channel_aliases'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('source_id')) { + context.handle( + _sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta), + ); + } else if (isInserting) { + context.missing(_sourceIdMeta); + } + if (data.containsKey('provider_channel_id')) { + context.handle( + _providerChannelIdMeta, + providerChannelId.isAcceptableOrUnknown( + data['provider_channel_id']!, + _providerChannelIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_providerChannelIdMeta); + } + if (data.containsKey('canonical_channel_id')) { + context.handle( + _canonicalChannelIdMeta, + canonicalChannelId.isAcceptableOrUnknown( + data['canonical_channel_id']!, + _canonicalChannelIdMeta, + ), + ); + } + if (data.containsKey('provider_name')) { + context.handle( + _providerNameMeta, + providerName.isAcceptableOrUnknown( + data['provider_name']!, + _providerNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_providerNameMeta); + } + if (data.containsKey('normalized_provider_name')) { + context.handle( + _normalizedProviderNameMeta, + normalizedProviderName.isAcceptableOrUnknown( + data['normalized_provider_name']!, + _normalizedProviderNameMeta, + ), + ); + } else if (isInserting) { + context.missing(_normalizedProviderNameMeta); + } + if (data.containsKey('tvg_id')) { + context.handle( + _tvgIdMeta, + tvgId.isAcceptableOrUnknown(data['tvg_id']!, _tvgIdMeta), + ); + } + if (data.containsKey('group_title')) { + context.handle( + _groupTitleMeta, + groupTitle.isAcceptableOrUnknown(data['group_title']!, _groupTitleMeta), + ); + } + if (data.containsKey('stream_url_fingerprint')) { + context.handle( + _streamUrlFingerprintMeta, + streamUrlFingerprint.isAcceptableOrUnknown( + data['stream_url_fingerprint']!, + _streamUrlFingerprintMeta, + ), + ); + } else if (isInserting) { + context.missing(_streamUrlFingerprintMeta); + } + if (data.containsKey('resolution')) { + context.handle( + _resolutionMeta, + resolution.isAcceptableOrUnknown(data['resolution']!, _resolutionMeta), + ); + } + if (data.containsKey('is_vod')) { + context.handle( + _isVodMeta, + isVod.isAcceptableOrUnknown(data['is_vod']!, _isVodMeta), + ); + } + if (data.containsKey('is_radio')) { + context.handle( + _isRadioMeta, + isRadio.isAcceptableOrUnknown(data['is_radio']!, _isRadioMeta), + ); + } + if (data.containsKey('is_adult')) { + context.handle( + _isAdultMeta, + isAdult.isAcceptableOrUnknown(data['is_adult']!, _isAdultMeta), + ); + } + if (data.containsKey('match_confidence')) { + context.handle( + _matchConfidenceMeta, + matchConfidence.isAcceptableOrUnknown( + data['match_confidence']!, + _matchConfidenceMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {sourceId, providerChannelId}; + @override + ProviderChannelAlias map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProviderChannelAlias( + sourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_id'], + )!, + providerChannelId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_channel_id'], + )!, + canonicalChannelId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}canonical_channel_id'], + ), + providerName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_name'], + )!, + normalizedProviderName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}normalized_provider_name'], + )!, + tvgId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}tvg_id'], + ), + groupTitle: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_title'], + ), + streamUrlFingerprint: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stream_url_fingerprint'], + )!, + resolution: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}resolution'], + ), + isVod: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_vod'], + )!, + isRadio: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_radio'], + )!, + isAdult: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_adult'], + )!, + matchConfidence: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}match_confidence'], + ), + ); + } + + @override + $ProviderChannelAliasesTable createAlias(String alias) { + return $ProviderChannelAliasesTable(attachedDatabase, alias); + } +} + +class ProviderChannelAlias extends DataClass + implements Insertable { + final String sourceId; + final String providerChannelId; + final String? canonicalChannelId; + final String providerName; + final String normalizedProviderName; + final int? tvgId; + final String? groupTitle; + final String streamUrlFingerprint; + final String? resolution; + final bool isVod; + final bool isRadio; + final bool isAdult; + final String? matchConfidence; + const ProviderChannelAlias({ + required this.sourceId, + required this.providerChannelId, + this.canonicalChannelId, + required this.providerName, + required this.normalizedProviderName, + this.tvgId, + this.groupTitle, + required this.streamUrlFingerprint, + this.resolution, + required this.isVod, + required this.isRadio, + required this.isAdult, + this.matchConfidence, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['source_id'] = Variable(sourceId); + map['provider_channel_id'] = Variable(providerChannelId); + if (!nullToAbsent || canonicalChannelId != null) { + map['canonical_channel_id'] = Variable(canonicalChannelId); + } + map['provider_name'] = Variable(providerName); + map['normalized_provider_name'] = Variable(normalizedProviderName); + if (!nullToAbsent || tvgId != null) { + map['tvg_id'] = Variable(tvgId); + } + if (!nullToAbsent || groupTitle != null) { + map['group_title'] = Variable(groupTitle); + } + map['stream_url_fingerprint'] = Variable(streamUrlFingerprint); + if (!nullToAbsent || resolution != null) { + map['resolution'] = Variable(resolution); + } + map['is_vod'] = Variable(isVod); + map['is_radio'] = Variable(isRadio); + map['is_adult'] = Variable(isAdult); + if (!nullToAbsent || matchConfidence != null) { + map['match_confidence'] = Variable(matchConfidence); + } + return map; + } + + ProviderChannelAliasesCompanion toCompanion(bool nullToAbsent) { + return ProviderChannelAliasesCompanion( + sourceId: Value(sourceId), + providerChannelId: Value(providerChannelId), + canonicalChannelId: canonicalChannelId == null && nullToAbsent + ? const Value.absent() + : Value(canonicalChannelId), + providerName: Value(providerName), + normalizedProviderName: Value(normalizedProviderName), + tvgId: tvgId == null && nullToAbsent + ? const Value.absent() + : Value(tvgId), + groupTitle: groupTitle == null && nullToAbsent + ? const Value.absent() + : Value(groupTitle), + streamUrlFingerprint: Value(streamUrlFingerprint), + resolution: resolution == null && nullToAbsent + ? const Value.absent() + : Value(resolution), + isVod: Value(isVod), + isRadio: Value(isRadio), + isAdult: Value(isAdult), + matchConfidence: matchConfidence == null && nullToAbsent + ? const Value.absent() + : Value(matchConfidence), + ); + } + + factory ProviderChannelAlias.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProviderChannelAlias( + sourceId: serializer.fromJson(json['sourceId']), + providerChannelId: serializer.fromJson(json['providerChannelId']), + canonicalChannelId: serializer.fromJson( + json['canonicalChannelId'], + ), + providerName: serializer.fromJson(json['providerName']), + normalizedProviderName: serializer.fromJson( + json['normalizedProviderName'], + ), + tvgId: serializer.fromJson(json['tvgId']), + groupTitle: serializer.fromJson(json['groupTitle']), + streamUrlFingerprint: serializer.fromJson( + json['streamUrlFingerprint'], + ), + resolution: serializer.fromJson(json['resolution']), + isVod: serializer.fromJson(json['isVod']), + isRadio: serializer.fromJson(json['isRadio']), + isAdult: serializer.fromJson(json['isAdult']), + matchConfidence: serializer.fromJson(json['matchConfidence']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sourceId': serializer.toJson(sourceId), + 'providerChannelId': serializer.toJson(providerChannelId), + 'canonicalChannelId': serializer.toJson(canonicalChannelId), + 'providerName': serializer.toJson(providerName), + 'normalizedProviderName': serializer.toJson( + normalizedProviderName, + ), + 'tvgId': serializer.toJson(tvgId), + 'groupTitle': serializer.toJson(groupTitle), + 'streamUrlFingerprint': serializer.toJson(streamUrlFingerprint), + 'resolution': serializer.toJson(resolution), + 'isVod': serializer.toJson(isVod), + 'isRadio': serializer.toJson(isRadio), + 'isAdult': serializer.toJson(isAdult), + 'matchConfidence': serializer.toJson(matchConfidence), + }; + } + + ProviderChannelAlias copyWith({ + String? sourceId, + String? providerChannelId, + Value canonicalChannelId = const Value.absent(), + String? providerName, + String? normalizedProviderName, + Value tvgId = const Value.absent(), + Value groupTitle = const Value.absent(), + String? streamUrlFingerprint, + Value resolution = const Value.absent(), + bool? isVod, + bool? isRadio, + bool? isAdult, + Value matchConfidence = const Value.absent(), + }) => ProviderChannelAlias( + sourceId: sourceId ?? this.sourceId, + providerChannelId: providerChannelId ?? this.providerChannelId, + canonicalChannelId: canonicalChannelId.present + ? canonicalChannelId.value + : this.canonicalChannelId, + providerName: providerName ?? this.providerName, + normalizedProviderName: + normalizedProviderName ?? this.normalizedProviderName, + tvgId: tvgId.present ? tvgId.value : this.tvgId, + groupTitle: groupTitle.present ? groupTitle.value : this.groupTitle, + streamUrlFingerprint: streamUrlFingerprint ?? this.streamUrlFingerprint, + resolution: resolution.present ? resolution.value : this.resolution, + isVod: isVod ?? this.isVod, + isRadio: isRadio ?? this.isRadio, + isAdult: isAdult ?? this.isAdult, + matchConfidence: matchConfidence.present + ? matchConfidence.value + : this.matchConfidence, + ); + ProviderChannelAlias copyWithCompanion(ProviderChannelAliasesCompanion data) { + return ProviderChannelAlias( + sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + providerChannelId: data.providerChannelId.present + ? data.providerChannelId.value + : this.providerChannelId, + canonicalChannelId: data.canonicalChannelId.present + ? data.canonicalChannelId.value + : this.canonicalChannelId, + providerName: data.providerName.present + ? data.providerName.value + : this.providerName, + normalizedProviderName: data.normalizedProviderName.present + ? data.normalizedProviderName.value + : this.normalizedProviderName, + tvgId: data.tvgId.present ? data.tvgId.value : this.tvgId, + groupTitle: data.groupTitle.present + ? data.groupTitle.value + : this.groupTitle, + streamUrlFingerprint: data.streamUrlFingerprint.present + ? data.streamUrlFingerprint.value + : this.streamUrlFingerprint, + resolution: data.resolution.present + ? data.resolution.value + : this.resolution, + isVod: data.isVod.present ? data.isVod.value : this.isVod, + isRadio: data.isRadio.present ? data.isRadio.value : this.isRadio, + isAdult: data.isAdult.present ? data.isAdult.value : this.isAdult, + matchConfidence: data.matchConfidence.present + ? data.matchConfidence.value + : this.matchConfidence, + ); + } + + @override + String toString() { + return (StringBuffer('ProviderChannelAlias(') + ..write('sourceId: $sourceId, ') + ..write('providerChannelId: $providerChannelId, ') + ..write('canonicalChannelId: $canonicalChannelId, ') + ..write('providerName: $providerName, ') + ..write('normalizedProviderName: $normalizedProviderName, ') + ..write('tvgId: $tvgId, ') + ..write('groupTitle: $groupTitle, ') + ..write('streamUrlFingerprint: $streamUrlFingerprint, ') + ..write('resolution: $resolution, ') + ..write('isVod: $isVod, ') + ..write('isRadio: $isRadio, ') + ..write('isAdult: $isAdult, ') + ..write('matchConfidence: $matchConfidence') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + sourceId, + providerChannelId, + canonicalChannelId, + providerName, + normalizedProviderName, + tvgId, + groupTitle, + streamUrlFingerprint, + resolution, + isVod, + isRadio, + isAdult, + matchConfidence, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProviderChannelAlias && + other.sourceId == this.sourceId && + other.providerChannelId == this.providerChannelId && + other.canonicalChannelId == this.canonicalChannelId && + other.providerName == this.providerName && + other.normalizedProviderName == this.normalizedProviderName && + other.tvgId == this.tvgId && + other.groupTitle == this.groupTitle && + other.streamUrlFingerprint == this.streamUrlFingerprint && + other.resolution == this.resolution && + other.isVod == this.isVod && + other.isRadio == this.isRadio && + other.isAdult == this.isAdult && + other.matchConfidence == this.matchConfidence); +} + +class ProviderChannelAliasesCompanion + extends UpdateCompanion { + final Value sourceId; + final Value providerChannelId; + final Value canonicalChannelId; + final Value providerName; + final Value normalizedProviderName; + final Value tvgId; + final Value groupTitle; + final Value streamUrlFingerprint; + final Value resolution; + final Value isVod; + final Value isRadio; + final Value isAdult; + final Value matchConfidence; + final Value rowid; + const ProviderChannelAliasesCompanion({ + this.sourceId = const Value.absent(), + this.providerChannelId = const Value.absent(), + this.canonicalChannelId = const Value.absent(), + this.providerName = const Value.absent(), + this.normalizedProviderName = const Value.absent(), + this.tvgId = const Value.absent(), + this.groupTitle = const Value.absent(), + this.streamUrlFingerprint = const Value.absent(), + this.resolution = const Value.absent(), + this.isVod = const Value.absent(), + this.isRadio = const Value.absent(), + this.isAdult = const Value.absent(), + this.matchConfidence = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProviderChannelAliasesCompanion.insert({ + required String sourceId, + required String providerChannelId, + this.canonicalChannelId = const Value.absent(), + required String providerName, + required String normalizedProviderName, + this.tvgId = const Value.absent(), + this.groupTitle = const Value.absent(), + required String streamUrlFingerprint, + this.resolution = const Value.absent(), + this.isVod = const Value.absent(), + this.isRadio = const Value.absent(), + this.isAdult = const Value.absent(), + this.matchConfidence = const Value.absent(), + this.rowid = const Value.absent(), + }) : sourceId = Value(sourceId), + providerChannelId = Value(providerChannelId), + providerName = Value(providerName), + normalizedProviderName = Value(normalizedProviderName), + streamUrlFingerprint = Value(streamUrlFingerprint); + static Insertable custom({ + Expression? sourceId, + Expression? providerChannelId, + Expression? canonicalChannelId, + Expression? providerName, + Expression? normalizedProviderName, + Expression? tvgId, + Expression? groupTitle, + Expression? streamUrlFingerprint, + Expression? resolution, + Expression? isVod, + Expression? isRadio, + Expression? isAdult, + Expression? matchConfidence, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (sourceId != null) 'source_id': sourceId, + if (providerChannelId != null) 'provider_channel_id': providerChannelId, + if (canonicalChannelId != null) + 'canonical_channel_id': canonicalChannelId, + if (providerName != null) 'provider_name': providerName, + if (normalizedProviderName != null) + 'normalized_provider_name': normalizedProviderName, + if (tvgId != null) 'tvg_id': tvgId, + if (groupTitle != null) 'group_title': groupTitle, + if (streamUrlFingerprint != null) + 'stream_url_fingerprint': streamUrlFingerprint, + if (resolution != null) 'resolution': resolution, + if (isVod != null) 'is_vod': isVod, + if (isRadio != null) 'is_radio': isRadio, + if (isAdult != null) 'is_adult': isAdult, + if (matchConfidence != null) 'match_confidence': matchConfidence, + if (rowid != null) 'rowid': rowid, + }); + } + + ProviderChannelAliasesCompanion copyWith({ + Value? sourceId, + Value? providerChannelId, + Value? canonicalChannelId, + Value? providerName, + Value? normalizedProviderName, + Value? tvgId, + Value? groupTitle, + Value? streamUrlFingerprint, + Value? resolution, + Value? isVod, + Value? isRadio, + Value? isAdult, + Value? matchConfidence, + Value? rowid, + }) { + return ProviderChannelAliasesCompanion( + sourceId: sourceId ?? this.sourceId, + providerChannelId: providerChannelId ?? this.providerChannelId, + canonicalChannelId: canonicalChannelId ?? this.canonicalChannelId, + providerName: providerName ?? this.providerName, + normalizedProviderName: + normalizedProviderName ?? this.normalizedProviderName, + tvgId: tvgId ?? this.tvgId, + groupTitle: groupTitle ?? this.groupTitle, + streamUrlFingerprint: streamUrlFingerprint ?? this.streamUrlFingerprint, + resolution: resolution ?? this.resolution, + isVod: isVod ?? this.isVod, + isRadio: isRadio ?? this.isRadio, + isAdult: isAdult ?? this.isAdult, + matchConfidence: matchConfidence ?? this.matchConfidence, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (providerChannelId.present) { + map['provider_channel_id'] = Variable(providerChannelId.value); + } + if (canonicalChannelId.present) { + map['canonical_channel_id'] = Variable(canonicalChannelId.value); + } + if (providerName.present) { + map['provider_name'] = Variable(providerName.value); + } + if (normalizedProviderName.present) { + map['normalized_provider_name'] = Variable( + normalizedProviderName.value, + ); + } + if (tvgId.present) { + map['tvg_id'] = Variable(tvgId.value); + } + if (groupTitle.present) { + map['group_title'] = Variable(groupTitle.value); + } + if (streamUrlFingerprint.present) { + map['stream_url_fingerprint'] = Variable( + streamUrlFingerprint.value, + ); + } + if (resolution.present) { + map['resolution'] = Variable(resolution.value); + } + if (isVod.present) { + map['is_vod'] = Variable(isVod.value); + } + if (isRadio.present) { + map['is_radio'] = Variable(isRadio.value); + } + if (isAdult.present) { + map['is_adult'] = Variable(isAdult.value); + } + if (matchConfidence.present) { + map['match_confidence'] = Variable(matchConfidence.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProviderChannelAliasesCompanion(') + ..write('sourceId: $sourceId, ') + ..write('providerChannelId: $providerChannelId, ') + ..write('canonicalChannelId: $canonicalChannelId, ') + ..write('providerName: $providerName, ') + ..write('normalizedProviderName: $normalizedProviderName, ') + ..write('tvgId: $tvgId, ') + ..write('groupTitle: $groupTitle, ') + ..write('streamUrlFingerprint: $streamUrlFingerprint, ') + ..write('resolution: $resolution, ') + ..write('isVod: $isVod, ') + ..write('isRadio: $isRadio, ') + ..write('isAdult: $isAdult, ') + ..write('matchConfidence: $matchConfidence, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$CanonicalChannelDatabase extends GeneratedDatabase { + _$CanonicalChannelDatabase(QueryExecutor e) : super(e); + $CanonicalChannelDatabaseManager get managers => + $CanonicalChannelDatabaseManager(this); + late final $CanonicalChannelsTable canonicalChannels = + $CanonicalChannelsTable(this); + late final $ProviderChannelAliasesTable providerChannelAliases = + $ProviderChannelAliasesTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + canonicalChannels, + providerChannelAliases, + ]; +} + +typedef $$CanonicalChannelsTableCreateCompanionBuilder = + CanonicalChannelsCompanion Function({ + required String canonicalChannelId, + required String displayName, + required String normalizedName, + Value language, + Value country, + Value category, + Value logoFingerprint, + required DateTime createdAt, + required DateTime updatedAt, + Value rowid, + }); +typedef $$CanonicalChannelsTableUpdateCompanionBuilder = + CanonicalChannelsCompanion Function({ + Value canonicalChannelId, + Value displayName, + Value normalizedName, + Value language, + Value country, + Value category, + Value logoFingerprint, + Value createdAt, + Value updatedAt, + Value rowid, + }); + +class $$CanonicalChannelsTableFilterComposer + extends Composer<_$CanonicalChannelDatabase, $CanonicalChannelsTable> { + $$CanonicalChannelsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get normalizedName => $composableBuilder( + column: $table.normalizedName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get language => $composableBuilder( + column: $table.language, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get country => $composableBuilder( + column: $table.country, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get logoFingerprint => $composableBuilder( + column: $table.logoFingerprint, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CanonicalChannelsTableOrderingComposer + extends Composer<_$CanonicalChannelDatabase, $CanonicalChannelsTable> { + $$CanonicalChannelsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get normalizedName => $composableBuilder( + column: $table.normalizedName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get language => $composableBuilder( + column: $table.language, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get country => $composableBuilder( + column: $table.country, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get logoFingerprint => $composableBuilder( + column: $table.logoFingerprint, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CanonicalChannelsTableAnnotationComposer + extends Composer<_$CanonicalChannelDatabase, $CanonicalChannelsTable> { + $$CanonicalChannelsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => column, + ); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); + + GeneratedColumn get normalizedName => $composableBuilder( + column: $table.normalizedName, + builder: (column) => column, + ); + + GeneratedColumn get language => + $composableBuilder(column: $table.language, builder: (column) => column); + + GeneratedColumn get country => + $composableBuilder(column: $table.country, builder: (column) => column); + + GeneratedColumn get category => + $composableBuilder(column: $table.category, builder: (column) => column); + + GeneratedColumn get logoFingerprint => $composableBuilder( + column: $table.logoFingerprint, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); +} + +class $$CanonicalChannelsTableTableManager + extends + RootTableManager< + _$CanonicalChannelDatabase, + $CanonicalChannelsTable, + CanonicalChannel, + $$CanonicalChannelsTableFilterComposer, + $$CanonicalChannelsTableOrderingComposer, + $$CanonicalChannelsTableAnnotationComposer, + $$CanonicalChannelsTableCreateCompanionBuilder, + $$CanonicalChannelsTableUpdateCompanionBuilder, + ( + CanonicalChannel, + BaseReferences< + _$CanonicalChannelDatabase, + $CanonicalChannelsTable, + CanonicalChannel + >, + ), + CanonicalChannel, + PrefetchHooks Function() + > { + $$CanonicalChannelsTableTableManager( + _$CanonicalChannelDatabase db, + $CanonicalChannelsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CanonicalChannelsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CanonicalChannelsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CanonicalChannelsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value canonicalChannelId = const Value.absent(), + Value displayName = const Value.absent(), + Value normalizedName = const Value.absent(), + Value language = const Value.absent(), + Value country = const Value.absent(), + Value category = const Value.absent(), + Value logoFingerprint = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => CanonicalChannelsCompanion( + canonicalChannelId: canonicalChannelId, + displayName: displayName, + normalizedName: normalizedName, + language: language, + country: country, + category: category, + logoFingerprint: logoFingerprint, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String canonicalChannelId, + required String displayName, + required String normalizedName, + Value language = const Value.absent(), + Value country = const Value.absent(), + Value category = const Value.absent(), + Value logoFingerprint = const Value.absent(), + required DateTime createdAt, + required DateTime updatedAt, + Value rowid = const Value.absent(), + }) => CanonicalChannelsCompanion.insert( + canonicalChannelId: canonicalChannelId, + displayName: displayName, + normalizedName: normalizedName, + language: language, + country: country, + category: category, + logoFingerprint: logoFingerprint, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CanonicalChannelsTableProcessedTableManager = + ProcessedTableManager< + _$CanonicalChannelDatabase, + $CanonicalChannelsTable, + CanonicalChannel, + $$CanonicalChannelsTableFilterComposer, + $$CanonicalChannelsTableOrderingComposer, + $$CanonicalChannelsTableAnnotationComposer, + $$CanonicalChannelsTableCreateCompanionBuilder, + $$CanonicalChannelsTableUpdateCompanionBuilder, + ( + CanonicalChannel, + BaseReferences< + _$CanonicalChannelDatabase, + $CanonicalChannelsTable, + CanonicalChannel + >, + ), + CanonicalChannel, + PrefetchHooks Function() + >; +typedef $$ProviderChannelAliasesTableCreateCompanionBuilder = + ProviderChannelAliasesCompanion Function({ + required String sourceId, + required String providerChannelId, + Value canonicalChannelId, + required String providerName, + required String normalizedProviderName, + Value tvgId, + Value groupTitle, + required String streamUrlFingerprint, + Value resolution, + Value isVod, + Value isRadio, + Value isAdult, + Value matchConfidence, + Value rowid, + }); +typedef $$ProviderChannelAliasesTableUpdateCompanionBuilder = + ProviderChannelAliasesCompanion Function({ + Value sourceId, + Value providerChannelId, + Value canonicalChannelId, + Value providerName, + Value normalizedProviderName, + Value tvgId, + Value groupTitle, + Value streamUrlFingerprint, + Value resolution, + Value isVod, + Value isRadio, + Value isAdult, + Value matchConfidence, + Value rowid, + }); + +class $$ProviderChannelAliasesTableFilterComposer + extends Composer<_$CanonicalChannelDatabase, $ProviderChannelAliasesTable> { + $$ProviderChannelAliasesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get sourceId => $composableBuilder( + column: $table.sourceId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerChannelId => $composableBuilder( + column: $table.providerChannelId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerName => $composableBuilder( + column: $table.providerName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get normalizedProviderName => $composableBuilder( + column: $table.normalizedProviderName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tvgId => $composableBuilder( + column: $table.tvgId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get groupTitle => $composableBuilder( + column: $table.groupTitle, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get streamUrlFingerprint => $composableBuilder( + column: $table.streamUrlFingerprint, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isVod => $composableBuilder( + column: $table.isVod, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isRadio => $composableBuilder( + column: $table.isRadio, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isAdult => $composableBuilder( + column: $table.isAdult, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get matchConfidence => $composableBuilder( + column: $table.matchConfidence, + builder: (column) => ColumnFilters(column), + ); +} + +class $$ProviderChannelAliasesTableOrderingComposer + extends Composer<_$CanonicalChannelDatabase, $ProviderChannelAliasesTable> { + $$ProviderChannelAliasesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get sourceId => $composableBuilder( + column: $table.sourceId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerChannelId => $composableBuilder( + column: $table.providerChannelId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerName => $composableBuilder( + column: $table.providerName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get normalizedProviderName => $composableBuilder( + column: $table.normalizedProviderName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tvgId => $composableBuilder( + column: $table.tvgId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get groupTitle => $composableBuilder( + column: $table.groupTitle, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get streamUrlFingerprint => $composableBuilder( + column: $table.streamUrlFingerprint, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isVod => $composableBuilder( + column: $table.isVod, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isRadio => $composableBuilder( + column: $table.isRadio, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isAdult => $composableBuilder( + column: $table.isAdult, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get matchConfidence => $composableBuilder( + column: $table.matchConfidence, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ProviderChannelAliasesTableAnnotationComposer + extends Composer<_$CanonicalChannelDatabase, $ProviderChannelAliasesTable> { + $$ProviderChannelAliasesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get sourceId => + $composableBuilder(column: $table.sourceId, builder: (column) => column); + + GeneratedColumn get providerChannelId => $composableBuilder( + column: $table.providerChannelId, + builder: (column) => column, + ); + + GeneratedColumn get canonicalChannelId => $composableBuilder( + column: $table.canonicalChannelId, + builder: (column) => column, + ); + + GeneratedColumn get providerName => $composableBuilder( + column: $table.providerName, + builder: (column) => column, + ); + + GeneratedColumn get normalizedProviderName => $composableBuilder( + column: $table.normalizedProviderName, + builder: (column) => column, + ); + + GeneratedColumn get tvgId => + $composableBuilder(column: $table.tvgId, builder: (column) => column); + + GeneratedColumn get groupTitle => $composableBuilder( + column: $table.groupTitle, + builder: (column) => column, + ); + + GeneratedColumn get streamUrlFingerprint => $composableBuilder( + column: $table.streamUrlFingerprint, + builder: (column) => column, + ); + + GeneratedColumn get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => column, + ); + + GeneratedColumn get isVod => + $composableBuilder(column: $table.isVod, builder: (column) => column); + + GeneratedColumn get isRadio => + $composableBuilder(column: $table.isRadio, builder: (column) => column); + + GeneratedColumn get isAdult => + $composableBuilder(column: $table.isAdult, builder: (column) => column); + + GeneratedColumn get matchConfidence => $composableBuilder( + column: $table.matchConfidence, + builder: (column) => column, + ); +} + +class $$ProviderChannelAliasesTableTableManager + extends + RootTableManager< + _$CanonicalChannelDatabase, + $ProviderChannelAliasesTable, + ProviderChannelAlias, + $$ProviderChannelAliasesTableFilterComposer, + $$ProviderChannelAliasesTableOrderingComposer, + $$ProviderChannelAliasesTableAnnotationComposer, + $$ProviderChannelAliasesTableCreateCompanionBuilder, + $$ProviderChannelAliasesTableUpdateCompanionBuilder, + ( + ProviderChannelAlias, + BaseReferences< + _$CanonicalChannelDatabase, + $ProviderChannelAliasesTable, + ProviderChannelAlias + >, + ), + ProviderChannelAlias, + PrefetchHooks Function() + > { + $$ProviderChannelAliasesTableTableManager( + _$CanonicalChannelDatabase db, + $ProviderChannelAliasesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProviderChannelAliasesTableFilterComposer( + $db: db, + $table: table, + ), + createOrderingComposer: () => + $$ProviderChannelAliasesTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$ProviderChannelAliasesTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value sourceId = const Value.absent(), + Value providerChannelId = const Value.absent(), + Value canonicalChannelId = const Value.absent(), + Value providerName = const Value.absent(), + Value normalizedProviderName = const Value.absent(), + Value tvgId = const Value.absent(), + Value groupTitle = const Value.absent(), + Value streamUrlFingerprint = const Value.absent(), + Value resolution = const Value.absent(), + Value isVod = const Value.absent(), + Value isRadio = const Value.absent(), + Value isAdult = const Value.absent(), + Value matchConfidence = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProviderChannelAliasesCompanion( + sourceId: sourceId, + providerChannelId: providerChannelId, + canonicalChannelId: canonicalChannelId, + providerName: providerName, + normalizedProviderName: normalizedProviderName, + tvgId: tvgId, + groupTitle: groupTitle, + streamUrlFingerprint: streamUrlFingerprint, + resolution: resolution, + isVod: isVod, + isRadio: isRadio, + isAdult: isAdult, + matchConfidence: matchConfidence, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String sourceId, + required String providerChannelId, + Value canonicalChannelId = const Value.absent(), + required String providerName, + required String normalizedProviderName, + Value tvgId = const Value.absent(), + Value groupTitle = const Value.absent(), + required String streamUrlFingerprint, + Value resolution = const Value.absent(), + Value isVod = const Value.absent(), + Value isRadio = const Value.absent(), + Value isAdult = const Value.absent(), + Value matchConfidence = const Value.absent(), + Value rowid = const Value.absent(), + }) => ProviderChannelAliasesCompanion.insert( + sourceId: sourceId, + providerChannelId: providerChannelId, + canonicalChannelId: canonicalChannelId, + providerName: providerName, + normalizedProviderName: normalizedProviderName, + tvgId: tvgId, + groupTitle: groupTitle, + streamUrlFingerprint: streamUrlFingerprint, + resolution: resolution, + isVod: isVod, + isRadio: isRadio, + isAdult: isAdult, + matchConfidence: matchConfidence, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$ProviderChannelAliasesTableProcessedTableManager = + ProcessedTableManager< + _$CanonicalChannelDatabase, + $ProviderChannelAliasesTable, + ProviderChannelAlias, + $$ProviderChannelAliasesTableFilterComposer, + $$ProviderChannelAliasesTableOrderingComposer, + $$ProviderChannelAliasesTableAnnotationComposer, + $$ProviderChannelAliasesTableCreateCompanionBuilder, + $$ProviderChannelAliasesTableUpdateCompanionBuilder, + ( + ProviderChannelAlias, + BaseReferences< + _$CanonicalChannelDatabase, + $ProviderChannelAliasesTable, + ProviderChannelAlias + >, + ), + ProviderChannelAlias, + PrefetchHooks Function() + >; + +class $CanonicalChannelDatabaseManager { + final _$CanonicalChannelDatabase _db; + $CanonicalChannelDatabaseManager(this._db); + $$CanonicalChannelsTableTableManager get canonicalChannels => + $$CanonicalChannelsTableTableManager(_db, _db.canonicalChannels); + $$ProviderChannelAliasesTableTableManager get providerChannelAliases => + $$ProviderChannelAliasesTableTableManager( + _db, + _db.providerChannelAliases, + ); +} diff --git a/packages/platform_playlist/lib/src/persistence/canonical_channel_repository.dart b/packages/platform_playlist/lib/src/persistence/canonical_channel_repository.dart new file mode 100644 index 00000000..6cabd845 --- /dev/null +++ b/packages/platform_playlist/lib/src/persistence/canonical_channel_repository.dart @@ -0,0 +1,61 @@ +import 'package:drift/drift.dart'; + +import 'canonical_channel_database.dart'; + +/// Repository over [CanonicalChannelDatabase] -- the persistence layer for +/// CV-017's canonical channel identity / provider alias contract. Kept +/// separate from [CanonicalChannelMatcher]/[FavoriteChannelRemapper] +/// (scoring, pure) so those stay unit-testable without a database. +class CanonicalChannelRepository { + CanonicalChannelRepository(this._db); + + final CanonicalChannelDatabase _db; + + Future upsertCanonical(CanonicalChannelsCompanion channel) { + return _db.into(_db.canonicalChannels).insertOnConflictUpdate(channel); + } + + Future getCanonical(String canonicalChannelId) { + return (_db.select(_db.canonicalChannels) + ..where((t) => t.canonicalChannelId.equals(canonicalChannelId))) + .getSingleOrNull(); + } + + Future> listCanonical() { + return _db.select(_db.canonicalChannels).get(); + } + + Future upsertAlias(ProviderChannelAliasesCompanion alias) { + return _db.into(_db.providerChannelAliases).insertOnConflictUpdate(alias); + } + + Future aliasFor({ + required String sourceId, + required String providerChannelId, + }) { + return (_db.select(_db.providerChannelAliases)..where( + (t) => + t.sourceId.equals(sourceId) & + t.providerChannelId.equals(providerChannelId), + )) + .getSingleOrNull(); + } + + Future> aliasesForCanonical( + String canonicalChannelId, + ) { + return (_db.select( + _db.providerChannelAliases, + )..where((t) => t.canonicalChannelId.equals(canonicalChannelId))).get(); + } + + /// Aliases sharing [tvgId], across every provider -- the same signal + /// [CanonicalChannelMatcher] treats as a high-confidence match. + Future> aliasesByTvgId(int tvgId) { + return (_db.select( + _db.providerChannelAliases, + )..where((t) => t.tvgId.equals(tvgId))).get(); + } + + Future close() => _db.close(); +} diff --git a/packages/platform_playlist/pubspec.yaml b/packages/platform_playlist/pubspec.yaml index f29b3c89..25970448 100644 --- a/packages/platform_playlist/pubspec.yaml +++ b/packages/platform_playlist/pubspec.yaml @@ -13,15 +13,22 @@ dependencies: core_data: path: ../core_data dio: ^5.10.0 + drift: ^2.23.1 equatable: ^2.0.8 + path: ^1.9.0 + path_provider: ^2.1.5 platform_channels: path: ../platform_channels platform_epg: path: ../platform_epg platform_playlist_import: path: ../platform_playlist_import + sqlite3: ^2.7.6 + sqlite3_flutter_libs: ^0.5.32 dev_dependencies: + build_runner: ^2.4.13 + drift_dev: ^2.23.1 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/platform_playlist/test/canonical_channel_repository_test.dart b/packages/platform_playlist/test/canonical_channel_repository_test.dart new file mode 100644 index 00000000..e87fbc58 --- /dev/null +++ b/packages/platform_playlist/test/canonical_channel_repository_test.dart @@ -0,0 +1,166 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_playlist/platform_playlist.dart'; + +void main() { + late CanonicalChannelDatabase db; + late CanonicalChannelRepository repository; + + setUp(() { + db = CanonicalChannelDatabase.forTesting(); + repository = CanonicalChannelRepository(db); + }); + + tearDown(() async { + await repository.close(); + }); + + CanonicalChannelsCompanion canonical({ + required String id, + required String displayName, + required String normalizedName, + }) { + final now = DateTime.utc(2026, 7, 19); + return CanonicalChannelsCompanion.insert( + canonicalChannelId: id, + displayName: displayName, + normalizedName: normalizedName, + createdAt: now, + updatedAt: now, + ); + } + + ProviderChannelAliasesCompanion alias({ + required String sourceId, + required String providerChannelId, + String? canonicalChannelId, + int? tvgId, + }) { + return ProviderChannelAliasesCompanion.insert( + sourceId: sourceId, + providerChannelId: providerChannelId, + canonicalChannelId: Value(canonicalChannelId), + providerName: 'BBC One HD', + normalizedProviderName: 'bbc one', + tvgId: Value(tvgId), + streamUrlFingerprint: 'fp-$sourceId-$providerChannelId', + ); + } + + test('upsertCanonical then getCanonical round-trips', () async { + await repository.upsertCanonical( + canonical(id: 'c1', displayName: 'BBC One', normalizedName: 'bbc one'), + ); + + final result = await repository.getCanonical('c1'); + + expect(result?.displayName, 'BBC One'); + expect(result?.normalizedName, 'bbc one'); + }); + + test('getCanonical returns null when nothing was stored', () async { + final result = await repository.getCanonical('missing'); + + expect(result, isNull); + }); + + test('upsertCanonical on the same id overwrites the previous row', () async { + await repository.upsertCanonical( + canonical(id: 'c1', displayName: 'BBC One', normalizedName: 'bbc one'), + ); + await repository.upsertCanonical( + canonical( + id: 'c1', + displayName: 'BBC One Updated', + normalizedName: 'bbc one', + ), + ); + + final result = await repository.getCanonical('c1'); + + expect(result?.displayName, 'BBC One Updated'); + final all = await repository.listCanonical(); + expect(all, hasLength(1)); + }); + + test('upsertAlias then aliasFor round-trips', () async { + await repository.upsertAlias( + alias(sourceId: 'provider-a', providerChannelId: 'a1', tvgId: 101), + ); + + final result = await repository.aliasFor( + sourceId: 'provider-a', + providerChannelId: 'a1', + ); + + expect(result?.tvgId, 101); + expect(result?.providerName, 'BBC One HD'); + }); + + test( + 'aliasFor returns null when nothing matches the composite key', + () async { + await repository.upsertAlias( + alias(sourceId: 'provider-a', providerChannelId: 'a1'), + ); + + final result = await repository.aliasFor( + sourceId: 'provider-b', + providerChannelId: 'a1', + ); + + expect(result, isNull); + }, + ); + + test( + 'aliasesForCanonical returns every alias pointing at a canonical id', + () async { + await repository.upsertCanonical( + canonical(id: 'c1', displayName: 'BBC One', normalizedName: 'bbc one'), + ); + await repository.upsertAlias( + alias( + sourceId: 'provider-a', + providerChannelId: 'a1', + canonicalChannelId: 'c1', + ), + ); + await repository.upsertAlias( + alias( + sourceId: 'provider-b', + providerChannelId: 'b1', + canonicalChannelId: 'c1', + ), + ); + await repository.upsertAlias( + alias(sourceId: 'provider-c', providerChannelId: 'c1'), + ); + + final aliases = await repository.aliasesForCanonical('c1'); + + expect(aliases, hasLength(2)); + expect(aliases.map((a) => a.providerChannelId).toSet(), {'a1', 'b1'}); + }, + ); + + test('aliasesByTvgId finds aliases across different providers', () async { + await repository.upsertAlias( + alias(sourceId: 'provider-a', providerChannelId: 'a1', tvgId: 101), + ); + await repository.upsertAlias( + alias(sourceId: 'provider-b', providerChannelId: 'b1', tvgId: 101), + ); + await repository.upsertAlias( + alias(sourceId: 'provider-c', providerChannelId: 'c1', tvgId: 202), + ); + + final matches = await repository.aliasesByTvgId(101); + + expect(matches, hasLength(2)); + expect(matches.map((a) => a.sourceId).toSet(), { + 'provider-a', + 'provider-b', + }); + }); +}