feat: configure pocketbase, generate dart types, update playback to use server instead of hive cache

This commit currently turns off sponsor block segment for compatibility reasons
This commit is contained in:
Kingkor Roy Tirtho 2023-02-01 22:05:37 +06:00
parent 84d94b05bc
commit ad90c11ab0
19 changed files with 369 additions and 35 deletions

View File

@ -28,6 +28,7 @@ jobs:
curl -sS https://webi.sh/yq | sh
yq -i '.version |= sub("\+\d+", "-nightly-")' pubspec.yaml
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
echo '${{ secrets.DOT_ENV }}' > .env
flutter config --enable-linux-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
@ -63,6 +64,7 @@ jobs:
curl -sS https://webi.sh/yq | sh
yq -i '.version |= sub("\+\d+", "-nightly-")' pubspec.yaml
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
echo '${{ secrets.DOT_ENV }}' > .env
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
@ -93,6 +95,7 @@ jobs:
yq -i '.version |= sub("\+\d+", "-nightly-")' pubspec.yaml
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.GITHUB_RUN_NUMBER }}/" windows/runner/Runner.rc
echo '${{ secrets.DOT_ENV }}' > .env
flutter config --enable-windows-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
@ -120,6 +123,7 @@ jobs:
- run: brew install yq
- run: yq -i '.version |= sub("\+\d+", "-nightly-")' pubspec.yaml
- run: yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- run: echo '${{ secrets.DOT_ENV }}' > .env
- run: flutter config --enable-macos-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'

View File

@ -31,6 +31,7 @@ jobs:
with:
cache: true
- run: |
echo '${{ secrets.DOT_ENV }}' > .env
flutter config --enable-windows-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
@ -72,6 +73,7 @@ jobs:
- uses: subosito/flutter-action@v2.8.0
with:
cache: true
- run: echo '${{ secrets.DOT_ENV }}' > .env
- run: flutter config --enable-macos-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
@ -112,6 +114,7 @@ jobs:
# replacing & adding new release version with older version
- run: |
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${{ steps.tag.outputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
echo '${{ secrets.DOT_ENV }}' > .env
- run: |
flutter config --enable-linux-desktop
@ -146,6 +149,7 @@ jobs:
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
- run: |
echo '${{ secrets.DOT_ENV }}' > .env
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks

2
.gitignore vendored
View File

@ -75,3 +75,5 @@ appimage-build
android/key.properties
.fvm/flutter_sdk
**/pb_data

11
lib/collections/env.dart Normal file
View File

@ -0,0 +1,11 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
abstract class Env {
static final String pocketbaseUrl = dotenv.get('POCKETBASE_URL');
static final String username = dotenv.get('USERNAME');
static final String password = dotenv.get('PASSWORD');
static configure() async {
await dotenv.load(fileName: ".env");
}
}

View File

@ -1,4 +1,5 @@
import 'package:spotube/entities/cache_track.dart';
import 'package:spotube/models/track.dart';
import 'package:spotube/utils/duration.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
@ -30,6 +31,11 @@ extension VideoFromCacheTrackExtension on Video {
false,
);
}
static Future<Video> fromBackendTrack(
BackendTrack track, YoutubeExplode youtube) {
return youtube.videos.get(VideoId.fromString(track.youtubeId));
}
}
extension ThumbnailSetJson on ThumbnailSet {

View File

@ -12,6 +12,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/cache_keys.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/entities/cache_track.dart';
import 'package:spotube/collections/routes.dart';
@ -23,6 +24,7 @@ import 'package:spotube/provider/playback_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_provider.dart';
import 'package:spotube/services/mobile_audio_service.dart';
import 'package:spotube/services/pocketbase.dart';
import 'package:spotube/themes/dark_theme.dart';
import 'package:spotube/themes/light_theme.dart';
import 'package:spotube/utils/platform.dart';
@ -36,6 +38,8 @@ void main() async {
Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter());
Hive.registerAdapter(CacheTrackSkipSegmentAdapter());
await Env.configure();
await initializePocketBase();
if (kIsDesktop) {
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
@ -72,7 +76,17 @@ void main() async {
enableApplicationParameters: false,
),
FileHandler(await getLogsPath(), printLogs: false),
SnackbarHandler(const Duration(seconds: 5)),
SnackbarHandler(
const Duration(seconds: 5),
action: SnackBarAction(
label: "Dismiss",
onPressed: () {
ScaffoldMessenger.of(
Catcher.navigatorKey!.currentContext!,
).hideCurrentSnackBar();
},
),
),
],
),
releaseConfig: CatcherOptions(SilentReportMode(), [

29
lib/models/track.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pocketbase/pocketbase.dart';
part 'track.g.dart';
@JsonSerializable()
class BackendTrack extends RecordModel {
@JsonKey(name: "spotify_id")
final String spotifyId;
@JsonKey(name: "youtube_id")
final String youtubeId;
final int votes;
BackendTrack({
required this.spotifyId,
required this.youtubeId,
required this.votes,
});
factory BackendTrack.fromRecord(RecordModel record) =>
BackendTrack.fromJson(record.toJson());
factory BackendTrack.fromJson(Map<String, dynamic> json) =>
_$BackendTrackFromJson(json);
@override
Map<String, dynamic> toJson() => _$BackendTrackToJson(this);
static String collection = "tracks";
}

30
lib/models/track.g.dart Normal file
View File

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'track.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BackendTrack _$BackendTrackFromJson(Map<String, dynamic> json) => BackendTrack(
spotifyId: json['spotify_id'] as String,
youtubeId: json['youtube_id'] as String,
votes: json['votes'] as int,
)
..id = json['id'] as String
..created = json['created'] as String
..updated = json['updated'] as String
..collectionId = json['collectionId'] as String
..collectionName = json['collectionName'] as String;
Map<String, dynamic> _$BackendTrackToJson(BackendTrack instance) =>
<String, dynamic>{
'id': instance.id,
'created': instance.created,
'updated': instance.updated,
'collectionId': instance.collectionId,
'collectionName': instance.collectionName,
'spotify_id': instance.spotifyId,
'youtube_id': instance.youtubeId,
'votes': instance.votes,
};

View File

@ -6,19 +6,19 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:catcher/catcher.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/entities/cache_track.dart';
import 'package:spotube/extensions/video.dart';
import 'package:spotube/models/current_playlist.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/models/track.dart';
import 'package:spotube/provider/audio_player_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_provider.dart';
import 'package:spotube/services/linux_audio_service.dart';
import 'package:spotube/services/mobile_audio_service.dart';
import 'package:spotube/services/pocketbase.dart';
import 'package:spotube/utils/persisted_change_notifier.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart';
@ -63,7 +63,6 @@ class Playback extends PersistedChangeNotifier {
ref.read(BlackListNotifier.provider.notifier);
// playlist & track list properties
late LazyBox<CacheTrack> cache;
CurrentPlaylist? playlist;
SpotubeTrack? track;
List<Video> _siblingYtVideos = [];
@ -94,8 +93,6 @@ class Playback extends PersistedChangeNotifier {
}
(() async {
cache = await Hive.openLazyBox<CacheTrack>("track-cache");
if (kIsAndroid) {
await player.setVolume(1);
volume = 1;
@ -386,7 +383,15 @@ class Playback extends PersistedChangeNotifier {
bool noSponsorBlock = false,
bool overwriteCache = false,
}) async {
final cachedTrack = await cache.get(track.id);
final cachedTracks = await pb
.collection(BackendTrack.collection)
.getFullList(filter: "spotify_id = '${track.id}'", sort: "-votes");
final cachedTrack = cachedTracks.isNotEmpty
? BackendTrack.fromRecord(cachedTracks.first)
: null;
final altTrack = cachedTracks.firstWhereOrNull(
(record) => record.data["youtube_id"] == ytVideo.id.value,
);
StreamManifest trackManifest = await raceMultiple(
() => youtube.videos.streams.getManifest(ytVideo.id),
);
@ -412,30 +417,43 @@ class Playback extends PersistedChangeNotifier {
final ytUri = chosenStreamInfo.url.toString();
final skipSegments = cachedTrack?.skipSegments != null &&
cachedTrack!.skipSegments!.isNotEmpty
? cachedTrack.skipSegments!
.map(
(segment) => segment.toJson(),
)
.toList()
: noSponsorBlock
? List.castFrom<dynamic, Map<String, int>>([])
: await getSkipSegments(ytVideo.id.value);
// final skipSegments =
// cachedTrack.skipSegments != null && cachedTrack.skipSegments!.isNotEmpty
// ? cachedTrack.skipSegments!
// .map(
// (segment) => segment.toJson(),
// )
// .toList()
// : noSponsorBlock
// ? List.castFrom<dynamic, Map<String, int>>([])
// : await getSkipSegments(ytVideo.id.value);
// only save when the track isn't available in the cache with same
// matchAlgorithm
if (overwriteCache ||
cachedTrack == null ||
cachedTrack.mode != preferences.trackMatchAlgorithm.name) {
await cache.put(
track.id!,
CacheTrack.fromVideo(
ytVideo,
preferences.trackMatchAlgorithm.name,
skipSegments: skipSegments,
),
if (cachedTrack == null && altTrack == null) {
await pb.collection(BackendTrack.collection).create(
body: BackendTrack(
spotifyId: track.id!,
youtubeId: ytVideo.id.value,
votes: 0,
).toJson(),
);
} else if (cachedTrack != null && altTrack != null && overwriteCache) {
await pb.collection(BackendTrack.collection).update(
altTrack.id,
body: {
"votes": altTrack.data["votes"] + 1,
},
);
} else if (cachedTrack != null && altTrack == null && overwriteCache) {
await pb.collection(BackendTrack.collection).create(
body: BackendTrack(
spotifyId: track.id!,
youtubeId: ytVideo.id.value,
votes: 1,
).toJson(),
);
}
return Tuple2(
@ -446,7 +464,7 @@ class Playback extends PersistedChangeNotifier {
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms
ytUri: ytUri,
skipSegments: skipSegments,
skipSegments: /* skipSegments */ [],
),
chosenStreamInfo,
);
@ -481,14 +499,17 @@ class Playback extends PersistedChangeNotifier {
_logger.v("[Youtube Search Term] $queryString");
Video ytVideo;
final cachedTrack = await cache.get(track.id);
if (cachedTrack != null &&
cachedTrack.mode == matchAlgorithm.name &&
!ignoreCache) {
final cachedTrack = await pb
.collection(BackendTrack.collection)
.getFullList(filter: "spotify_id = '${track.id}'", sort: "-votes")
.then((l) => l.isNotEmpty ? BackendTrack.fromRecord(l.first) : null);
if (cachedTrack != null && !ignoreCache) {
_logger.v(
"[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
"[Playing track from cache] youtubeId: ${cachedTrack.youtubeId}",
);
ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
ytVideo = await VideoFromCacheTrackExtension.fromBackendTrack(
cachedTrack, youtube);
} else {
VideoSearchList videos =
await raceMultiple(() => youtube.search.search(queryString));

View File

@ -0,0 +1,14 @@
import 'package:catcher/catcher.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:spotube/collections/env.dart';
final pb = PocketBase(Env.pocketbaseUrl);
bool isLoggedIn = false;
Future<void> initializePocketBase() async {
try {
await pb.collection("users").authWithPassword(Env.username, Env.password);
isLoggedIn = true;
} catch (e, stack) {
Catcher.reportCheckedError(e, stack);
}
}

View File

@ -575,6 +575,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_feather_icons:
dependency: "direct main"
description:
@ -822,12 +829,19 @@ packages:
source: hosted
version: "0.6.4"
json_annotation:
dependency: transitive
dependency: "direct main"
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.8.0"
json_serializable:
dependency: "direct main"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "6.6.0"
libadwaita:
dependency: "direct main"
description:
@ -1103,6 +1117,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
pocketbase:
dependency: "direct main"
description:
name: pocketbase
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.1+1"
pointycastle:
dependency: transitive
description:

View File

@ -31,6 +31,7 @@ dependencies:
fluentui_system_icons: ^1.1.189
flutter:
sdk: flutter
flutter_dotenv: ^5.0.2
flutter_feather_icons: ^2.0.0+1
flutter_hooks: ^0.18.2+1
flutter_inappwebview: ^5.7.2+3
@ -44,6 +45,8 @@ dependencies:
html: ^0.15.1
http: ^0.13.5
introduction_screen: ^3.0.2
json_annotation: ^4.8.0
json_serializable: ^6.6.0
libadwaita: ^1.2.5
logger: ^1.1.0
macos_ui: ^1.7.5
@ -59,6 +62,7 @@ dependencies:
git:
url: https://github.com/KRTirtho/platform_ui.git
ref: 073cefb9c419fcb01cbdfd6ca2f9714eec23c83b
pocketbase: ^0.7.1+1
popover: ^0.2.6+3
queue: ^3.1.0+1
scroll_to_index: ^3.0.1
@ -96,6 +100,7 @@ flutter:
assets:
- assets/
- assets/tutorial/
- .env
flutter_icons:
android: true

1
server/.pocketbase Normal file
View File

@ -0,0 +1 @@
version=0.12.1

View File

@ -0,0 +1,63 @@
migrate((db) => {
const collection = new Collection({
"id": "pevn93oxbnovw0s",
"created": "2023-02-01 13:01:08.893Z",
"updated": "2023-02-01 13:01:08.893Z",
"name": "tracks",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "ycnix0ai",
"name": "spotify_id",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": 20,
"max": 22,
"pattern": ""
}
},
{
"system": false,
"id": "ih8fxzgh",
"name": "youtube_id",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": 10,
"max": 11,
"pattern": ""
}
},
{
"system": false,
"id": "vzvqgsjf",
"name": "votes",
"type": "number",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null
}
}
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
});
return Dao(db).saveCollection(collection);
}, (db) => {
const dao = new Dao(db);
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s");
return dao.deleteCollection(collection);
})

View File

@ -0,0 +1,17 @@
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.listRule = ""
collection.viewRule = ""
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.listRule = null
collection.viewRule = null
return dao.saveCollection(collection)
})

View File

@ -0,0 +1,19 @@
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
collection.createRule = null
collection.updateRule = null
collection.deleteRule = null
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
collection.createRule = ""
collection.updateRule = "id = @request.auth.id"
collection.deleteRule = "id = @request.auth.id"
return dao.saveCollection(collection)
})

View File

@ -0,0 +1,17 @@
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.createRule = "@request.auth.id != ''"
collection.updateRule = "@request.auth.id != ''"
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.createRule = null
collection.updateRule = null
return dao.saveCollection(collection)
})

View File

@ -0,0 +1,17 @@
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.createRule = "@request.auth.id != '' && ((spotify_id ?= @collection.tracks.spotify_id && youtube_id ?= @collection.tracks.youtube_id) || (spotify_id ?!= @collection.tracks.spotify_id && youtube_id ?!= @collection.tracks.youtube_id))"
collection.updateRule = "@request.auth.id != '' && ((spotify_id ?= @collection.tracks.spotify_id && youtube_id ?= @collection.tracks.youtube_id) || (spotify_id ?!= @collection.tracks.spotify_id && youtube_id ?!= @collection.tracks.youtube_id))"
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
collection.createRule = null
collection.updateRule = null
return dao.saveCollection(collection)
})

View File

@ -0,0 +1,39 @@
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "vzvqgsjf",
"name": "votes",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
}))
return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("pevn93oxbnovw0s")
// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "vzvqgsjf",
"name": "votes",
"type": "number",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null
}
}))
return dao.saveCollection(collection)
})