Remote provider basis (jellyfin)

This commit is contained in:
2025-12-19 23:34:20 +08:00
parent ac4ceb3960
commit 2f1130b424
9 changed files with 1202 additions and 7 deletions

View File

@@ -42,6 +42,17 @@ class WatchFolders extends Table {
DateTimeColumn get lastScanned => dateTime().nullable()(); DateTimeColumn get lastScanned => dateTime().nullable()();
} }
class RemoteProviders extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get serverUrl => text().unique()();
TextColumn get name => text()();
TextColumn get username => text()();
TextColumn get password =>
text()(); // Note: In production, this should be encrypted
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
DateTimeColumn get addedAt => dateTime().withDefault(currentDateAndTime)();
}
class AppSettings extends Table { class AppSettings extends Table {
TextColumn get key => text()(); TextColumn get key => text()();
TextColumn get value => text()(); TextColumn get value => text()();
@@ -51,13 +62,20 @@ class AppSettings extends Table {
} }
@DriftDatabase( @DriftDatabase(
tables: [Tracks, Playlists, PlaylistEntries, WatchFolders, AppSettings], tables: [
Tracks,
Playlists,
PlaylistEntries,
WatchFolders,
RemoteProviders,
AppSettings,
],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 6; // Bump version for watch folders and settings int get schemaVersion => 7; // Bump version for remote providers
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@@ -84,6 +102,10 @@ class AppDatabase extends _$AppDatabase {
await m.createTable(watchFolders); await m.createTable(watchFolders);
await m.createTable(appSettings); await m.createTable(appSettings);
} }
if (from < 7) {
// Create table for remote providers
await m.createTable(remoteProviders);
}
}, },
); );
} }

View File

@@ -1597,6 +1597,451 @@ class WatchFoldersCompanion extends UpdateCompanion<WatchFolder> {
} }
} }
class $RemoteProvidersTable extends RemoteProviders
with TableInfo<$RemoteProvidersTable, RemoteProvider> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$RemoteProvidersTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id',
aliasedName,
false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'PRIMARY KEY AUTOINCREMENT',
),
);
static const VerificationMeta _serverUrlMeta = const VerificationMeta(
'serverUrl',
);
@override
late final GeneratedColumn<String> serverUrl = GeneratedColumn<String>(
'server_url',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'),
);
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _usernameMeta = const VerificationMeta(
'username',
);
@override
late final GeneratedColumn<String> username = GeneratedColumn<String>(
'username',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _passwordMeta = const VerificationMeta(
'password',
);
@override
late final GeneratedColumn<String> password = GeneratedColumn<String>(
'password',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _isActiveMeta = const VerificationMeta(
'isActive',
);
@override
late final GeneratedColumn<bool> isActive = GeneratedColumn<bool>(
'is_active',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_active" IN (0, 1))',
),
defaultValue: const Constant(true),
);
static const VerificationMeta _addedAtMeta = const VerificationMeta(
'addedAt',
);
@override
late final GeneratedColumn<DateTime> addedAt = GeneratedColumn<DateTime>(
'added_at',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: currentDateAndTime,
);
@override
List<GeneratedColumn> get $columns => [
id,
serverUrl,
name,
username,
password,
isActive,
addedAt,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'remote_providers';
@override
VerificationContext validateIntegrity(
Insertable<RemoteProvider> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('server_url')) {
context.handle(
_serverUrlMeta,
serverUrl.isAcceptableOrUnknown(data['server_url']!, _serverUrlMeta),
);
} else if (isInserting) {
context.missing(_serverUrlMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta,
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
);
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('username')) {
context.handle(
_usernameMeta,
username.isAcceptableOrUnknown(data['username']!, _usernameMeta),
);
} else if (isInserting) {
context.missing(_usernameMeta);
}
if (data.containsKey('password')) {
context.handle(
_passwordMeta,
password.isAcceptableOrUnknown(data['password']!, _passwordMeta),
);
} else if (isInserting) {
context.missing(_passwordMeta);
}
if (data.containsKey('is_active')) {
context.handle(
_isActiveMeta,
isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta),
);
}
if (data.containsKey('added_at')) {
context.handle(
_addedAtMeta,
addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta),
);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
RemoteProvider map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return RemoteProvider(
id: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}id'],
)!,
serverUrl: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}server_url'],
)!,
name: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
username: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}username'],
)!,
password: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}password'],
)!,
isActive: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_active'],
)!,
addedAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}added_at'],
)!,
);
}
@override
$RemoteProvidersTable createAlias(String alias) {
return $RemoteProvidersTable(attachedDatabase, alias);
}
}
class RemoteProvider extends DataClass implements Insertable<RemoteProvider> {
final int id;
final String serverUrl;
final String name;
final String username;
final String password;
final bool isActive;
final DateTime addedAt;
const RemoteProvider({
required this.id,
required this.serverUrl,
required this.name,
required this.username,
required this.password,
required this.isActive,
required this.addedAt,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['server_url'] = Variable<String>(serverUrl);
map['name'] = Variable<String>(name);
map['username'] = Variable<String>(username);
map['password'] = Variable<String>(password);
map['is_active'] = Variable<bool>(isActive);
map['added_at'] = Variable<DateTime>(addedAt);
return map;
}
RemoteProvidersCompanion toCompanion(bool nullToAbsent) {
return RemoteProvidersCompanion(
id: Value(id),
serverUrl: Value(serverUrl),
name: Value(name),
username: Value(username),
password: Value(password),
isActive: Value(isActive),
addedAt: Value(addedAt),
);
}
factory RemoteProvider.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return RemoteProvider(
id: serializer.fromJson<int>(json['id']),
serverUrl: serializer.fromJson<String>(json['serverUrl']),
name: serializer.fromJson<String>(json['name']),
username: serializer.fromJson<String>(json['username']),
password: serializer.fromJson<String>(json['password']),
isActive: serializer.fromJson<bool>(json['isActive']),
addedAt: serializer.fromJson<DateTime>(json['addedAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'serverUrl': serializer.toJson<String>(serverUrl),
'name': serializer.toJson<String>(name),
'username': serializer.toJson<String>(username),
'password': serializer.toJson<String>(password),
'isActive': serializer.toJson<bool>(isActive),
'addedAt': serializer.toJson<DateTime>(addedAt),
};
}
RemoteProvider copyWith({
int? id,
String? serverUrl,
String? name,
String? username,
String? password,
bool? isActive,
DateTime? addedAt,
}) => RemoteProvider(
id: id ?? this.id,
serverUrl: serverUrl ?? this.serverUrl,
name: name ?? this.name,
username: username ?? this.username,
password: password ?? this.password,
isActive: isActive ?? this.isActive,
addedAt: addedAt ?? this.addedAt,
);
RemoteProvider copyWithCompanion(RemoteProvidersCompanion data) {
return RemoteProvider(
id: data.id.present ? data.id.value : this.id,
serverUrl: data.serverUrl.present ? data.serverUrl.value : this.serverUrl,
name: data.name.present ? data.name.value : this.name,
username: data.username.present ? data.username.value : this.username,
password: data.password.present ? data.password.value : this.password,
isActive: data.isActive.present ? data.isActive.value : this.isActive,
addedAt: data.addedAt.present ? data.addedAt.value : this.addedAt,
);
}
@override
String toString() {
return (StringBuffer('RemoteProvider(')
..write('id: $id, ')
..write('serverUrl: $serverUrl, ')
..write('name: $name, ')
..write('username: $username, ')
..write('password: $password, ')
..write('isActive: $isActive, ')
..write('addedAt: $addedAt')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, serverUrl, name, username, password, isActive, addedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is RemoteProvider &&
other.id == this.id &&
other.serverUrl == this.serverUrl &&
other.name == this.name &&
other.username == this.username &&
other.password == this.password &&
other.isActive == this.isActive &&
other.addedAt == this.addedAt);
}
class RemoteProvidersCompanion extends UpdateCompanion<RemoteProvider> {
final Value<int> id;
final Value<String> serverUrl;
final Value<String> name;
final Value<String> username;
final Value<String> password;
final Value<bool> isActive;
final Value<DateTime> addedAt;
const RemoteProvidersCompanion({
this.id = const Value.absent(),
this.serverUrl = const Value.absent(),
this.name = const Value.absent(),
this.username = const Value.absent(),
this.password = const Value.absent(),
this.isActive = const Value.absent(),
this.addedAt = const Value.absent(),
});
RemoteProvidersCompanion.insert({
this.id = const Value.absent(),
required String serverUrl,
required String name,
required String username,
required String password,
this.isActive = const Value.absent(),
this.addedAt = const Value.absent(),
}) : serverUrl = Value(serverUrl),
name = Value(name),
username = Value(username),
password = Value(password);
static Insertable<RemoteProvider> custom({
Expression<int>? id,
Expression<String>? serverUrl,
Expression<String>? name,
Expression<String>? username,
Expression<String>? password,
Expression<bool>? isActive,
Expression<DateTime>? addedAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (serverUrl != null) 'server_url': serverUrl,
if (name != null) 'name': name,
if (username != null) 'username': username,
if (password != null) 'password': password,
if (isActive != null) 'is_active': isActive,
if (addedAt != null) 'added_at': addedAt,
});
}
RemoteProvidersCompanion copyWith({
Value<int>? id,
Value<String>? serverUrl,
Value<String>? name,
Value<String>? username,
Value<String>? password,
Value<bool>? isActive,
Value<DateTime>? addedAt,
}) {
return RemoteProvidersCompanion(
id: id ?? this.id,
serverUrl: serverUrl ?? this.serverUrl,
name: name ?? this.name,
username: username ?? this.username,
password: password ?? this.password,
isActive: isActive ?? this.isActive,
addedAt: addedAt ?? this.addedAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (serverUrl.present) {
map['server_url'] = Variable<String>(serverUrl.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (username.present) {
map['username'] = Variable<String>(username.value);
}
if (password.present) {
map['password'] = Variable<String>(password.value);
}
if (isActive.present) {
map['is_active'] = Variable<bool>(isActive.value);
}
if (addedAt.present) {
map['added_at'] = Variable<DateTime>(addedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('RemoteProvidersCompanion(')
..write('id: $id, ')
..write('serverUrl: $serverUrl, ')
..write('name: $name, ')
..write('username: $username, ')
..write('password: $password, ')
..write('isActive: $isActive, ')
..write('addedAt: $addedAt')
..write(')'))
.toString();
}
}
class $AppSettingsTable extends AppSettings class $AppSettingsTable extends AppSettings
with TableInfo<$AppSettingsTable, AppSetting> { with TableInfo<$AppSettingsTable, AppSetting> {
@override @override
@@ -1814,6 +2259,9 @@ abstract class _$AppDatabase extends GeneratedDatabase {
this, this,
); );
late final $WatchFoldersTable watchFolders = $WatchFoldersTable(this); late final $WatchFoldersTable watchFolders = $WatchFoldersTable(this);
late final $RemoteProvidersTable remoteProviders = $RemoteProvidersTable(
this,
);
late final $AppSettingsTable appSettings = $AppSettingsTable(this); late final $AppSettingsTable appSettings = $AppSettingsTable(this);
@override @override
Iterable<TableInfo<Table, Object?>> get allTables => Iterable<TableInfo<Table, Object?>> get allTables =>
@@ -1824,6 +2272,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
playlists, playlists,
playlistEntries, playlistEntries,
watchFolders, watchFolders,
remoteProviders,
appSettings, appSettings,
]; ];
@override @override
@@ -3118,6 +3567,244 @@ typedef $$WatchFoldersTableProcessedTableManager =
WatchFolder, WatchFolder,
PrefetchHooks Function() PrefetchHooks Function()
>; >;
typedef $$RemoteProvidersTableCreateCompanionBuilder =
RemoteProvidersCompanion Function({
Value<int> id,
required String serverUrl,
required String name,
required String username,
required String password,
Value<bool> isActive,
Value<DateTime> addedAt,
});
typedef $$RemoteProvidersTableUpdateCompanionBuilder =
RemoteProvidersCompanion Function({
Value<int> id,
Value<String> serverUrl,
Value<String> name,
Value<String> username,
Value<String> password,
Value<bool> isActive,
Value<DateTime> addedAt,
});
class $$RemoteProvidersTableFilterComposer
extends Composer<_$AppDatabase, $RemoteProvidersTable> {
$$RemoteProvidersTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get serverUrl => $composableBuilder(
column: $table.serverUrl,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get username => $composableBuilder(
column: $table.username,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get password => $composableBuilder(
column: $table.password,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get isActive => $composableBuilder(
column: $table.isActive,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get addedAt => $composableBuilder(
column: $table.addedAt,
builder: (column) => ColumnFilters(column),
);
}
class $$RemoteProvidersTableOrderingComposer
extends Composer<_$AppDatabase, $RemoteProvidersTable> {
$$RemoteProvidersTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get serverUrl => $composableBuilder(
column: $table.serverUrl,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get username => $composableBuilder(
column: $table.username,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get password => $composableBuilder(
column: $table.password,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get isActive => $composableBuilder(
column: $table.isActive,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get addedAt => $composableBuilder(
column: $table.addedAt,
builder: (column) => ColumnOrderings(column),
);
}
class $$RemoteProvidersTableAnnotationComposer
extends Composer<_$AppDatabase, $RemoteProvidersTable> {
$$RemoteProvidersTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get serverUrl =>
$composableBuilder(column: $table.serverUrl, builder: (column) => column);
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumn<String> get username =>
$composableBuilder(column: $table.username, builder: (column) => column);
GeneratedColumn<String> get password =>
$composableBuilder(column: $table.password, builder: (column) => column);
GeneratedColumn<bool> get isActive =>
$composableBuilder(column: $table.isActive, builder: (column) => column);
GeneratedColumn<DateTime> get addedAt =>
$composableBuilder(column: $table.addedAt, builder: (column) => column);
}
class $$RemoteProvidersTableTableManager
extends
RootTableManager<
_$AppDatabase,
$RemoteProvidersTable,
RemoteProvider,
$$RemoteProvidersTableFilterComposer,
$$RemoteProvidersTableOrderingComposer,
$$RemoteProvidersTableAnnotationComposer,
$$RemoteProvidersTableCreateCompanionBuilder,
$$RemoteProvidersTableUpdateCompanionBuilder,
(
RemoteProvider,
BaseReferences<
_$AppDatabase,
$RemoteProvidersTable,
RemoteProvider
>,
),
RemoteProvider,
PrefetchHooks Function()
> {
$$RemoteProvidersTableTableManager(
_$AppDatabase db,
$RemoteProvidersTable table,
) : super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$RemoteProvidersTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$RemoteProvidersTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$RemoteProvidersTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<int> id = const Value.absent(),
Value<String> serverUrl = const Value.absent(),
Value<String> name = const Value.absent(),
Value<String> username = const Value.absent(),
Value<String> password = const Value.absent(),
Value<bool> isActive = const Value.absent(),
Value<DateTime> addedAt = const Value.absent(),
}) => RemoteProvidersCompanion(
id: id,
serverUrl: serverUrl,
name: name,
username: username,
password: password,
isActive: isActive,
addedAt: addedAt,
),
createCompanionCallback:
({
Value<int> id = const Value.absent(),
required String serverUrl,
required String name,
required String username,
required String password,
Value<bool> isActive = const Value.absent(),
Value<DateTime> addedAt = const Value.absent(),
}) => RemoteProvidersCompanion.insert(
id: id,
serverUrl: serverUrl,
name: name,
username: username,
password: password,
isActive: isActive,
addedAt: addedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$RemoteProvidersTableProcessedTableManager =
ProcessedTableManager<
_$AppDatabase,
$RemoteProvidersTable,
RemoteProvider,
$$RemoteProvidersTableFilterComposer,
$$RemoteProvidersTableOrderingComposer,
$$RemoteProvidersTableAnnotationComposer,
$$RemoteProvidersTableCreateCompanionBuilder,
$$RemoteProvidersTableUpdateCompanionBuilder,
(
RemoteProvider,
BaseReferences<_$AppDatabase, $RemoteProvidersTable, RemoteProvider>,
),
RemoteProvider,
PrefetchHooks Function()
>;
typedef $$AppSettingsTableCreateCompanionBuilder = typedef $$AppSettingsTableCreateCompanionBuilder =
AppSettingsCompanion Function({ AppSettingsCompanion Function({
required String key, required String key,
@@ -3269,6 +3956,8 @@ class $AppDatabaseManager {
$$PlaylistEntriesTableTableManager(_db, _db.playlistEntries); $$PlaylistEntriesTableTableManager(_db, _db.playlistEntries);
$$WatchFoldersTableTableManager get watchFolders => $$WatchFoldersTableTableManager get watchFolders =>
$$WatchFoldersTableTableManager(_db, _db.watchFolders); $$WatchFoldersTableTableManager(_db, _db.watchFolders);
$$RemoteProvidersTableTableManager get remoteProviders =>
$$RemoteProvidersTableTableManager(_db, _db.remoteProviders);
$$AppSettingsTableTableManager get appSettings => $$AppSettingsTableTableManager get appSettings =>
$$AppSettingsTableTableManager(_db, _db.appSettings); $$AppSettingsTableTableManager(_db, _db.appSettings);
} }

View File

@@ -34,7 +34,7 @@ final class PlaylistRepositoryProvider
} }
String _$playlistRepositoryHash() => String _$playlistRepositoryHash() =>
r'614d837f9438d2454778edb4ff60b046418490b8'; r'20c2c56f237a9e3ac3efe1225d05db8264b19678';
abstract class _$PlaylistRepository extends $AsyncNotifier<void> { abstract class _$PlaylistRepository extends $AsyncNotifier<void> {
FutureOr<void> build(); FutureOr<void> build();

View File

@@ -33,7 +33,7 @@ final class TrackRepositoryProvider
TrackRepository create() => TrackRepository(); TrackRepository create() => TrackRepository();
} }
String _$trackRepositoryHash() => r'538fedbc358e305aac4517d2c517a8bdf6bbb75c'; String _$trackRepositoryHash() => r'655c231192698ef0c31920af846de47def7da81d';
abstract class _$TrackRepository extends $AsyncNotifier<void> { abstract class _$TrackRepository extends $AsyncNotifier<void> {
FutureOr<void> build(); FutureOr<void> build();

View File

@@ -0,0 +1,205 @@
import 'package:flutter/foundation.dart';
import 'package:groovybox/data/db.dart';
import 'package:groovybox/providers/db_provider.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:drift/drift.dart';
import 'package:jellyfin_dart/jellyfin_dart.dart';
// Simple remote provider using Riverpod
final remoteProvidersProvider = FutureProvider<List<RemoteProvider>>((
ref,
) async {
final db = ref.read(databaseProvider);
return await (db.select(
db.remoteProviders,
)..orderBy([(t) => OrderingTerm(expression: t.addedAt)])).get();
});
final activeRemoteProvidersProvider = Provider<List<RemoteProvider>>((ref) {
final remoteProvidersAsync = ref.watch(remoteProvidersProvider);
return remoteProvidersAsync.when(
data: (providers) =>
providers.where((provider) => provider.isActive).toList(),
loading: () => [],
error: (_, _) => [],
);
});
class RemoteProviderService {
final Ref ref;
RemoteProviderService(this.ref);
Future<void> addRemoteProvider(
String serverUrl,
String username,
String password, {
String? name,
}) async {
final db = ref.read(databaseProvider);
final providerName = name ?? Uri.parse(serverUrl).host;
await db
.into(db.remoteProviders)
.insert(
RemoteProvidersCompanion.insert(
serverUrl: serverUrl,
name: providerName,
username: username,
password: password,
),
);
// Invalidate the provider to refresh UI
ref.invalidate(remoteProvidersProvider);
}
Future<void> removeRemoteProvider(int providerId) async {
final db = ref.read(databaseProvider);
await (db.delete(
db.remoteProviders,
)..where((t) => t.id.equals(providerId))).go();
// Invalidate the provider to refresh UI
ref.invalidate(remoteProvidersProvider);
}
Future<void> toggleRemoteProvider(int providerId, bool isActive) async {
final db = ref.read(databaseProvider);
await (db.update(db.remoteProviders)..where((t) => t.id.equals(providerId)))
.write(RemoteProvidersCompanion(isActive: Value(isActive)));
// Invalidate the provider to refresh UI
ref.invalidate(remoteProvidersProvider);
}
Future<void> indexRemoteProvider(int providerId) async {
final db = ref.read(databaseProvider);
// Get provider details
final provider = await (db.select(
db.remoteProviders,
)..where((t) => t.id.equals(providerId))).getSingleOrNull();
if (provider == null) {
throw Exception('Remote provider not found: $providerId');
}
if (!provider.isActive) {
debugPrint('Provider $providerId is not active, skipping indexing');
return;
}
try {
// Create Jellyfin client
final client = JellyfinDart(basePathOverride: provider.serverUrl);
// Set device info
client.setDeviceId('groovybox-${providerId}');
client.setVersion('1.0.0');
// Authenticate
final userApi = client.getUserApi();
final authResponse = await userApi.authenticateUserByName(
authenticateUserByName: AuthenticateUserByName(
username: provider.username,
pw: provider.password,
),
);
final token = authResponse.data?.accessToken;
if (token == null) {
throw Exception('Authentication failed for provider ${provider.name}');
}
client.setToken(token);
// Fetch music items
final itemsApi = client.getItemsApi();
final musicItems = await itemsApi.getItems(
includeItemTypes: [BaseItemKind.audio],
recursive: true,
fields: [
ItemFields.path,
ItemFields.mediaStreams,
ItemFields.mediaSources,
ItemFields.genres,
ItemFields.tags,
ItemFields.overview,
],
);
final items = musicItems.data?.items ?? [];
// Convert to tracks and store
for (final item in items) {
await _addRemoteTrack(db, provider, item, token);
}
debugPrint('Indexed $items.length tracks from $provider.name');
} catch (e) {
debugPrint('Error indexing remote provider $provider.name: $e');
rethrow;
}
}
Future<void> _addRemoteTrack(
AppDatabase db,
RemoteProvider provider,
BaseItemDto item,
String token,
) async {
// Generate streaming URL
final streamUrl =
'${provider.serverUrl}/Audio/${item.id}/stream.mp3?api_key=$token&static=true';
// Extract metadata
final title = item.name ?? 'Unknown Title';
final artist =
item.albumArtist ?? item.artists?.join(', ') ?? 'Unknown Artist';
final album = item.album ?? 'Unknown Album';
final duration =
(item.runTimeTicks ?? 0) ~/ 10000; // Convert ticks to milliseconds
// Check if track already exists
final existingTrack = await (db.select(
db.tracks,
)..where((t) => t.path.equals(streamUrl))).getSingleOrNull();
if (existingTrack != null) {
// Update existing track
await (db.update(
db.tracks,
)..where((t) => t.id.equals(existingTrack.id))).write(
TracksCompanion(
title: Value(title),
artist: Value(artist),
album: Value(album),
duration: Value(duration),
addedAt: Value(DateTime.now()),
),
);
} else {
// Insert new track
await db
.into(db.tracks)
.insert(
TracksCompanion.insert(
title: title,
path: streamUrl, // Remote streaming URL
artist: Value(artist),
album: Value(album),
duration: Value(duration),
),
mode: InsertMode.insertOrIgnore,
);
}
}
}
// Provider for the service
final remoteProviderServiceProvider = Provider<RemoteProviderService>((ref) {
return RemoteProviderService(ref);
});

View File

@@ -87,7 +87,7 @@ final class ImportModeNotifierProvider
} }
String _$importModeNotifierHash() => String _$importModeNotifierHash() =>
r'eaf3dcf7c74dc24d6ebe14840d597e4a79859a63'; r'4a4f8d3bb378e964f1d67159a650a2d7addeab69';
abstract class _$ImportModeNotifier extends $Notifier<ImportMode> { abstract class _$ImportModeNotifier extends $Notifier<ImportMode> {
ImportMode build(); ImportMode build();
@@ -140,7 +140,7 @@ final class AutoScanNotifierProvider
} }
} }
String _$autoScanNotifierHash() => r'56f2f1a2f6aef095782a0ed4407a43a8f589dc4b'; String _$autoScanNotifierHash() => r'e8d7c9bd7059e0117979b120616addcd5c1abb8d';
abstract class _$AutoScanNotifier extends $Notifier<bool> { abstract class _$AutoScanNotifier extends $Notifier<bool> {
bool build(); bool build();
@@ -194,7 +194,7 @@ final class WatchForChangesNotifierProvider
} }
String _$watchForChangesNotifierHash() => String _$watchForChangesNotifierHash() =>
r'b4648380ae989e6e36138780d0c925916b6e20b3'; r'1f15ffac52a0401b14d8cd4e04d39c69d5a2e704';
abstract class _$WatchForChangesNotifier extends $Notifier<bool> { abstract class _$WatchForChangesNotifier extends $Notifier<bool> {
bool build(); bool build();

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:groovybox/providers/settings_provider.dart'; import 'package:groovybox/providers/settings_provider.dart';
import 'package:groovybox/providers/watch_folder_provider.dart'; import 'package:groovybox/providers/watch_folder_provider.dart';
import 'package:groovybox/providers/remote_provider.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -12,6 +13,7 @@ class SettingsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final settingsAsync = ref.watch(settingsProvider); final settingsAsync = ref.watch(settingsProvider);
final watchFoldersAsync = ref.watch(watchFoldersProvider); final watchFoldersAsync = ref.watch(watchFoldersProvider);
final remoteProvidersAsync = ref.watch(remoteProvidersProvider);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(title: const Text('Settings')),
@@ -79,6 +81,7 @@ class SettingsScreen extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -186,6 +189,122 @@ class SettingsScreen extends ConsumerWidget {
], ],
), ),
), ),
// Remote Providers Section
Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Remote Providers',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
IconButton(
onPressed: () =>
_indexRemoteProviders(context, ref),
icon: const Icon(Icons.refresh),
tooltip: 'Index Remote Providers',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
IconButton(
onPressed: () =>
_addRemoteProvider(context, ref),
icon: const Icon(Icons.add),
tooltip: 'Add Remote Provider',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
],
),
],
),
const Text(
'Connect to remote media servers like Jellyfin to access your music library.',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
).padding(horizontal: 16, top: 16, bottom: 8),
remoteProvidersAsync.when(
data: (providers) => providers.isEmpty
? const Text(
'No remote providers added yet.',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
).padding(horizontal: 16, vertical: 8)
: Column(
children: providers
.map(
(provider) => ListTile(
title: Text(provider.name),
subtitle: Text(provider.serverUrl),
contentPadding: const EdgeInsets.only(
left: 16,
right: 16,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Switch(
value: provider.isActive,
onChanged: (value) {
ref
.read(
remoteProviderServiceProvider,
)
.toggleRemoteProvider(
provider.id,
value,
);
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
ref
.read(
remoteProviderServiceProvider,
)
.removeRemoteProvider(
provider.id,
);
},
),
],
),
),
)
.toList(),
),
loading: () => const CircularProgressIndicator(),
error: (error, _) =>
Text('Error loading providers: $error'),
),
const SizedBox(height: 8),
],
),
),
], ],
), ),
), ),
@@ -237,4 +356,139 @@ class SettingsScreen extends ConsumerWidget {
} }
} }
} }
void _indexRemoteProviders(BuildContext context, WidgetRef ref) async {
try {
final service = ref.read(remoteProviderServiceProvider);
final providersAsync = ref.read(remoteProvidersProvider);
providersAsync.when(
data: (providers) async {
final activeProviders = providers.where((p) => p.isActive).toList();
if (activeProviders.isEmpty) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No active remote providers to index'),
),
);
}
return;
}
for (final provider in activeProviders) {
try {
await service.indexRemoteProvider(provider.id);
} catch (e) {
debugPrint('Error indexing provider ${provider.name}: $e');
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Indexed ${activeProviders.length} remote provider(s)',
),
),
);
}
},
loading: () {
// Providers are still loading, do nothing
},
error: (error, _) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading providers: $error')),
);
}
},
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error indexing remote providers: $e')),
);
}
}
}
void _addRemoteProvider(BuildContext context, WidgetRef ref) {
final serverUrlController = TextEditingController();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Remote Provider'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: serverUrlController,
decoration: const InputDecoration(
labelText: 'Server URL',
hintText: 'https://your-jellyfin-server.com',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
TextField(
controller: usernameController,
decoration: const InputDecoration(labelText: 'Username'),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
final serverUrl = serverUrlController.text.trim();
final username = usernameController.text.trim();
final password = passwordController.text.trim();
if (serverUrl.isEmpty || username.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('All fields are required')),
);
return;
}
try {
final service = ref.read(remoteProviderServiceProvider);
await service.addRemoteProvider(serverUrl, username, password);
if (context.mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Added remote provider: $serverUrl'),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error adding provider: $e')),
);
}
}
},
child: const Text('Add'),
),
],
),
);
}
} }

View File

@@ -233,6 +233,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
copy_with_extension:
dependency: transitive
description:
name: copy_with_extension
sha256: c9e09bce2fee69729ea55dbd55f7150d4cf6f7e55461091a02839c21346f1b95
url: "https://pub.dev"
source: hosted
version: "10.0.1"
coverage: coverage:
dependency: transitive dependency: transitive
description: description:
@@ -345,6 +353,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.8" version: "0.2.8"
equatable:
dependency: transitive
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -560,6 +576,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
jellyfin_dart:
dependency: "direct main"
description:
name: jellyfin_dart
sha256: "269fddfee36ee4a0fa2d03cf9e6f448d77ca35893faa77723c677889ac3f7e6f"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
js: js:
dependency: transitive dependency: transitive
description: description:

View File

@@ -55,6 +55,7 @@ dependencies:
palette_generator: ^0.3.3+4 palette_generator: ^0.3.3+4
watcher: ^1.2.0 watcher: ^1.2.0
shared_preferences: ^2.3.5 shared_preferences: ^2.3.5
jellyfin_dart: ^0.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: