mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: remove SponsorBlock in favor of YT Music and remove pocketbase backend track support
This commit is contained in:
parent
b058517912
commit
fb780da327
@ -1,7 +1,3 @@
|
|||||||
POCKETBASE_URL=
|
|
||||||
USERNAME=
|
|
||||||
PASSWORD=
|
|
||||||
|
|
||||||
# The format:
|
# The format:
|
||||||
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
|
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
|
||||||
SPOTIFY_SECRETS=
|
SPOTIFY_SECRETS=
|
||||||
|
@ -118,7 +118,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt
|
|||||||
|
|
||||||
Do the following:
|
Do the following:
|
||||||
|
|
||||||
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support
|
- Download the latest Flutter SDK (>=3.10.0) & enable desktop support
|
||||||
- Install Development dependencies in linux
|
- Install Development dependencies in linux
|
||||||
- Debian (>=12/Bookworm)/Ubuntu
|
- Debian (>=12/Bookworm)/Ubuntu
|
||||||
```bash
|
```bash
|
||||||
@ -137,7 +137,7 @@ Do the following:
|
|||||||
- Create a `.env` in root of the project following the `.env.example` template
|
- Create a `.env` in root of the project following the `.env.example` template
|
||||||
- Now run the following to bootstrap the project
|
- Now run the following to bootstrap the project
|
||||||
```bash
|
```bash
|
||||||
flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
|
flutter pub get && dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||||
```
|
```
|
||||||
- Finally run these following commands in the root of the project to start the Spotube Locally
|
- Finally run these following commands in the root of the project to start the Spotube Locally
|
||||||
```bash
|
```bash
|
||||||
|
@ -4,15 +4,6 @@ part 'env.g.dart';
|
|||||||
|
|
||||||
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
||||||
abstract class Env {
|
abstract class Env {
|
||||||
@EnviedField(varName: 'POCKETBASE_URL', defaultValue: 'http://127.0.0.1:8090')
|
|
||||||
static final pocketbaseUrl = _Env.pocketbaseUrl;
|
|
||||||
|
|
||||||
@EnviedField(varName: 'USERNAME', defaultValue: 'root')
|
|
||||||
static final username = _Env.username;
|
|
||||||
|
|
||||||
@EnviedField(varName: 'PASSWORD', defaultValue: '12345678')
|
|
||||||
static final password = _Env.password;
|
|
||||||
|
|
||||||
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
||||||
static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) {
|
static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) {
|
||||||
final secrets = e.trim().split(":").map((e) => e.trim());
|
final secrets = e.trim().split(":").map((e) => e.trim());
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import 'package:piped_client/piped_client.dart';
|
|
||||||
import 'package:spotube/models/track.dart';
|
|
||||||
import 'package:spotube/services/youtube.dart';
|
|
||||||
|
|
||||||
extension PipedStreamResponseExtension on PipedStreamResponse {
|
|
||||||
static Future<PipedStreamResponse> fromBackendTrack(
|
|
||||||
BackendTrack track) async {
|
|
||||||
return await PipedSpotube.client.streams(track.youtubeId);
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
@ -19,11 +20,11 @@ import 'package:spotube/collections/routes.dart';
|
|||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/l10n/l10n.dart';
|
import 'package:spotube/l10n/l10n.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
|
import 'package:spotube/models/matched_track.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
import 'package:spotube/provider/downloader_provider.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/pocketbase.dart';
|
|
||||||
import 'package:spotube/services/youtube.dart';
|
import 'package:spotube/services/youtube.dart';
|
||||||
import 'package:spotube/themes/theme.dart';
|
import 'package:spotube/themes/theme.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
@ -93,6 +94,13 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
cachePrefix: "oss.krtirtho.spotube",
|
cachePrefix: "oss.krtirtho.spotube",
|
||||||
cacheDir: (await getApplicationSupportDirectory()).path,
|
cacheDir: (await getApplicationSupportDirectory()).path,
|
||||||
);
|
);
|
||||||
|
Hive.registerAdapter(MatchedTrackAdapter());
|
||||||
|
|
||||||
|
await Hive.openLazyBox<MatchedTrack>(
|
||||||
|
MatchedTrack.boxName,
|
||||||
|
path: (await getApplicationSupportDirectory()).path,
|
||||||
|
);
|
||||||
|
|
||||||
await PersistedStateNotifier.initializeBoxes();
|
await PersistedStateNotifier.initializeBoxes();
|
||||||
|
|
||||||
Catcher(
|
Catcher(
|
||||||
@ -164,7 +172,6 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await initializePocketBase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Spotube extends StatefulHookConsumerWidget {
|
class Spotube extends StatefulHookConsumerWidget {
|
||||||
|
17
lib/models/matched_track.dart
Normal file
17
lib/models/matched_track.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import "package:hive/hive.dart";
|
||||||
|
|
||||||
|
part "matched_track.g.dart";
|
||||||
|
|
||||||
|
@HiveType(typeId: 1)
|
||||||
|
class MatchedTrack {
|
||||||
|
@HiveField(0)
|
||||||
|
String youtubeId;
|
||||||
|
@HiveField(1)
|
||||||
|
String spotifyId;
|
||||||
|
|
||||||
|
static const boxName = "oss.krtirtho.spotube.matched_tracks";
|
||||||
|
|
||||||
|
static LazyBox<MatchedTrack> get box => Hive.lazyBox<MatchedTrack>(boxName);
|
||||||
|
|
||||||
|
MatchedTrack({required this.youtubeId, required this.spotifyId});
|
||||||
|
}
|
44
lib/models/matched_track.g.dart
Normal file
44
lib/models/matched_track.g.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'matched_track.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class MatchedTrackAdapter extends TypeAdapter<MatchedTrack> {
|
||||||
|
@override
|
||||||
|
final int typeId = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MatchedTrack read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return MatchedTrack(
|
||||||
|
youtubeId: fields[0] as String,
|
||||||
|
spotifyId: fields[1] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, MatchedTrack obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(2)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.youtubeId)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.spotifyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is MatchedTrackAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
@ -1,45 +1,29 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:catcher/catcher.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:pocketbase/pocketbase.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/piped.dart';
|
|
||||||
import 'package:spotube/extensions/album_simple.dart';
|
import 'package:spotube/extensions/album_simple.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/matched_track.dart';
|
||||||
import 'package:spotube/models/track.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/pocketbase.dart';
|
|
||||||
import 'package:spotube/services/youtube.dart';
|
import 'package:spotube/services/youtube.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
enum SpotubeTrackMatchAlgorithm {
|
|
||||||
// selects the first result returned from YouTube
|
|
||||||
youtube,
|
|
||||||
// selects the most popular one
|
|
||||||
popular,
|
|
||||||
// selects the most popular one from the author of the track
|
|
||||||
authenticPopular,
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef SkipSegment = ({int start, int end});
|
typedef SkipSegment = ({int start, int end});
|
||||||
|
|
||||||
class SpotubeTrack extends Track {
|
class SpotubeTrack extends Track {
|
||||||
final PipedStreamResponse ytTrack;
|
final PipedStreamResponse ytTrack;
|
||||||
final String ytUri;
|
final String ytUri;
|
||||||
final List<SkipSegment> skipSegments;
|
|
||||||
final List<PipedSearchItemStream> siblings;
|
final List<PipedSearchItemStream> siblings;
|
||||||
|
|
||||||
SpotubeTrack(
|
SpotubeTrack(
|
||||||
this.ytTrack,
|
this.ytTrack,
|
||||||
this.ytUri,
|
this.ytUri,
|
||||||
this.skipSegments,
|
|
||||||
this.siblings,
|
this.siblings,
|
||||||
) : super();
|
) : super();
|
||||||
|
|
||||||
@ -47,7 +31,6 @@ class SpotubeTrack extends Track {
|
|||||||
required Track track,
|
required Track track,
|
||||||
required this.ytTrack,
|
required this.ytTrack,
|
||||||
required this.ytUri,
|
required this.ytUri,
|
||||||
required this.skipSegments,
|
|
||||||
required this.siblings,
|
required this.siblings,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
album = track.album;
|
album = track.album;
|
||||||
@ -84,53 +67,7 @@ class SpotubeTrack extends Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SkipSegment>> getSkipSegments(
|
static Future<List<PipedSearchItemStream>> fetchSiblings(Track track) async {
|
||||||
String id,
|
|
||||||
UserPreferences preferences,
|
|
||||||
) async {
|
|
||||||
if (!preferences.skipSponsorSegments) return [];
|
|
||||||
try {
|
|
||||||
final res = await get(Uri(
|
|
||||||
scheme: "https",
|
|
||||||
host: "sponsor.ajay.app",
|
|
||||||
path: "/api/skipSegments",
|
|
||||||
queryParameters: {
|
|
||||||
"videoID": id,
|
|
||||||
"category": [
|
|
||||||
'sponsor',
|
|
||||||
'selfpromo',
|
|
||||||
'interaction',
|
|
||||||
'intro',
|
|
||||||
'outro',
|
|
||||||
'music_offtopic'
|
|
||||||
],
|
|
||||||
"actionType": 'skip'
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
if (res.body == "Not Found") {
|
|
||||||
return List.castFrom<dynamic, SkipSegment>([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = jsonDecode(res.body) as List;
|
|
||||||
final segments = data.map((obj) {
|
|
||||||
return (
|
|
||||||
start: obj["segment"].first.toInt(),
|
|
||||||
end: obj["segment"].last.toInt(),
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
getLogger(SpotubeTrack).v(
|
|
||||||
"[SponsorBlock] successfully fetched skip segments for $id",
|
|
||||||
);
|
|
||||||
return List.castFrom<dynamic, SkipSegment>(segments);
|
|
||||||
} catch (e, stack) {
|
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return List.castFrom<dynamic, SkipSegment>([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<SpotubeTrack> fetchFromTrack(
|
|
||||||
Track track, UserPreferences preferences) async {
|
|
||||||
final artists = (track.artists ?? [])
|
final artists = (track.artists ?? [])
|
||||||
.map((ar) => ar.name)
|
.map((ar) => ar.name)
|
||||||
.toList()
|
.toList()
|
||||||
@ -143,69 +80,59 @@ class SpotubeTrack extends Track {
|
|||||||
onlyCleanArtist: true,
|
onlyCleanArtist: true,
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
final cachedTracks = await Future<RecordModel?>.value(
|
final List<PipedSearchItemStream> siblings = await PipedSpotube.client
|
||||||
pb
|
.search(
|
||||||
.collection(BackendTrack.collection)
|
"$title - ${artists.join(", ")}",
|
||||||
.getFirstListItem("spotify_id = '${track.id}'"),
|
PipedFilter.musicSongs,
|
||||||
).catchError((e, stack) {
|
)
|
||||||
return null;
|
.then(
|
||||||
});
|
(res) {
|
||||||
|
final siblings = res.items
|
||||||
|
.whereType<PipedSearchItemStream>()
|
||||||
|
.where((item) {
|
||||||
|
return artists.any(
|
||||||
|
(artist) =>
|
||||||
|
artist.toLowerCase() == item.uploaderName.toLowerCase(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.take(10)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final cachedTrack =
|
if (siblings.isEmpty) {
|
||||||
cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null;
|
return res.items.whereType<PipedSearchItemStream>().take(10).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return siblings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<SpotubeTrack> fetchFromTrack(
|
||||||
|
Track track,
|
||||||
|
UserPreferences preferences,
|
||||||
|
) async {
|
||||||
|
final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
|
||||||
|
var siblings = <PipedSearchItemStream>[];
|
||||||
PipedStreamResponse ytVideo;
|
PipedStreamResponse ytVideo;
|
||||||
PipedAudioStream ytStream;
|
if (matchedCachedTrack != null) {
|
||||||
List<PipedSearchItemStream> siblings = [];
|
ytVideo = await PipedSpotube.client.streams(matchedCachedTrack.youtubeId);
|
||||||
List<SkipSegment> skipSegments = [];
|
|
||||||
if (cachedTrack != null) {
|
|
||||||
final responses = await Future.wait(
|
|
||||||
[
|
|
||||||
PipedStreamResponseExtension.fromBackendTrack(cachedTrack),
|
|
||||||
if (preferences.skipSponsorSegments)
|
|
||||||
getSkipSegments(cachedTrack.youtubeId, preferences)
|
|
||||||
else
|
|
||||||
Future.value([])
|
|
||||||
],
|
|
||||||
);
|
|
||||||
ytVideo = responses.first as PipedStreamResponse;
|
|
||||||
skipSegments = responses.last as List<SkipSegment>;
|
|
||||||
ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
|
||||||
} else {
|
} else {
|
||||||
final videos = await PipedSpotube.client
|
siblings = await fetchSiblings(track);
|
||||||
.search("${artists.join(", ")} - $title", PipedFilter.musicSongs);
|
ytVideo = await PipedSpotube.client.streams(siblings.first.id);
|
||||||
// await PrimitiveUtils.raceMultiple(
|
|
||||||
// () => youtube.search.search("${artists.join(", ")} - $title"),
|
await MatchedTrack.box.put(
|
||||||
// );
|
track.id!,
|
||||||
siblings =
|
MatchedTrack(
|
||||||
videos.items.whereType<PipedSearchItemStream>().take(10).toList();
|
youtubeId: ytVideo.id,
|
||||||
final responses = await Future.wait(
|
spotifyId: track.id!,
|
||||||
[
|
),
|
||||||
PipedSpotube.client.streams(siblings.first.id),
|
|
||||||
if (preferences.skipSponsorSegments)
|
|
||||||
getSkipSegments(siblings.first.id, preferences)
|
|
||||||
else
|
|
||||||
Future.value([])
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
ytVideo = responses.first as PipedStreamResponse;
|
|
||||||
skipSegments = responses.last as List<SkipSegment>;
|
|
||||||
ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedTrack == null) {
|
final PipedAudioStream ytStream =
|
||||||
await Future<RecordModel?>.value(
|
getStreamInfo(ytVideo, preferences.audioQuality);
|
||||||
pb.collection(BackendTrack.collection).create(
|
|
||||||
body: BackendTrack(
|
|
||||||
spotifyId: track.id!,
|
|
||||||
youtubeId: ytVideo.id,
|
|
||||||
votes: 0,
|
|
||||||
).toJson(),
|
|
||||||
)).catchError((e, stack) {
|
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.predownload &&
|
if (preferences.predownload &&
|
||||||
ytVideo.duration < const Duration(minutes: 15)) {
|
ytVideo.duration < const Duration(minutes: 15)) {
|
||||||
@ -230,7 +157,6 @@ class SpotubeTrack extends Track {
|
|||||||
track: track,
|
track: track,
|
||||||
ytTrack: ytVideo,
|
ytTrack: ytVideo,
|
||||||
ytUri: ytStream.url,
|
ytUri: ytStream.url,
|
||||||
skipSegments: skipSegments,
|
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -241,59 +167,19 @@ class SpotubeTrack extends Track {
|
|||||||
) async {
|
) async {
|
||||||
if (siblings.none((element) => element.id == video.id)) return null;
|
if (siblings.none((element) => element.id == video.id)) return null;
|
||||||
|
|
||||||
final [PipedStreamResponse ytVideo, List<SkipSegment> skipSegments] =
|
final ytVideo = await PipedSpotube.client.streams(video.id);
|
||||||
await Future.wait<dynamic>(
|
|
||||||
[
|
|
||||||
PipedSpotube.client.streams(video.id),
|
|
||||||
if (preferences.skipSponsorSegments)
|
|
||||||
getSkipSegments(video.id, preferences)
|
|
||||||
else
|
|
||||||
Future.value(<Map<String, int>>[])
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// await PrimitiveUtils.raceMultiple(
|
|
||||||
// () => youtube.videos.streams.getManifest(video.id),
|
|
||||||
// );
|
|
||||||
|
|
||||||
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
||||||
|
|
||||||
final ytUri = ytStream.url;
|
final ytUri = ytStream.url;
|
||||||
|
|
||||||
final cachedTracks = await Future<RecordModel?>.value(
|
await MatchedTrack.box.put(
|
||||||
pb.collection(BackendTrack.collection).getFirstListItem(
|
id!,
|
||||||
"spotify_id = '$id' && youtube_id = '${video.id}'",
|
MatchedTrack(
|
||||||
),
|
|
||||||
).catchError((e, stack) {
|
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
final cachedTrack =
|
|
||||||
cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null;
|
|
||||||
|
|
||||||
if (cachedTrack == null) {
|
|
||||||
await Future<RecordModel?>.value(
|
|
||||||
pb.collection(BackendTrack.collection).create(
|
|
||||||
body: BackendTrack(
|
|
||||||
spotifyId: id!,
|
|
||||||
youtubeId: video.id,
|
youtubeId: video.id,
|
||||||
votes: 1,
|
spotifyId: id!,
|
||||||
).toJson(),
|
),
|
||||||
)).catchError((e, stack) {
|
);
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await Future<RecordModel?>.value(
|
|
||||||
pb.collection(BackendTrack.collection).update(
|
|
||||||
cachedTrack.id,
|
|
||||||
body: {"votes": cachedTrack.votes + 1},
|
|
||||||
)).catchError((e, stack) {
|
|
||||||
Catcher.reportCheckedError(e, stack);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.predownload &&
|
if (preferences.predownload &&
|
||||||
video.duration < const Duration(minutes: 15)) {
|
video.duration < const Duration(minutes: 15)) {
|
||||||
@ -318,7 +204,6 @@ class SpotubeTrack extends Track {
|
|||||||
track: this,
|
track: this,
|
||||||
ytTrack: ytVideo,
|
ytTrack: ytVideo,
|
||||||
ytUri: ytUri,
|
ytUri: ytUri,
|
||||||
skipSegments: skipSegments,
|
|
||||||
siblings: [
|
siblings: [
|
||||||
video,
|
video,
|
||||||
...siblings.where((element) => element.id != video.id),
|
...siblings.where((element) => element.id != video.id),
|
||||||
@ -331,7 +216,6 @@ class SpotubeTrack extends Track {
|
|||||||
track: Track.fromJson(map),
|
track: Track.fromJson(map),
|
||||||
ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
|
ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
|
||||||
ytUri: map["ytUri"],
|
ytUri: map["ytUri"],
|
||||||
skipSegments: List.castFrom<dynamic, SkipSegment>(map["skipSegments"]),
|
|
||||||
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
||||||
.map((sibling) => PipedSearchItemStream.fromJson(sibling))
|
.map((sibling) => PipedSearchItemStream.fromJson(sibling))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -340,30 +224,13 @@ class SpotubeTrack extends Track {
|
|||||||
|
|
||||||
Future<SpotubeTrack> populatedCopy() async {
|
Future<SpotubeTrack> populatedCopy() async {
|
||||||
if (this.siblings.isNotEmpty) return this;
|
if (this.siblings.isNotEmpty) return this;
|
||||||
final artists = (this.artists ?? [])
|
|
||||||
.map((ar) => ar.name)
|
|
||||||
.toList()
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final siblings = await fetchSiblings(this);
|
||||||
name!,
|
|
||||||
artists: artists,
|
|
||||||
onlyCleanArtist: true,
|
|
||||||
).trim();
|
|
||||||
final videos = await PipedSpotube.client.search(
|
|
||||||
"${artists.join(", ")} - $title",
|
|
||||||
PipedFilter.musicSongs,
|
|
||||||
);
|
|
||||||
|
|
||||||
final siblings =
|
|
||||||
videos.items.whereType<PipedSearchItemStream>().take(10).toList();
|
|
||||||
|
|
||||||
return SpotubeTrack.fromTrack(
|
return SpotubeTrack.fromTrack(
|
||||||
track: this,
|
track: this,
|
||||||
ytTrack: ytTrack,
|
ytTrack: ytTrack,
|
||||||
ytUri: ytUri,
|
ytUri: ytUri,
|
||||||
skipSegments: skipSegments,
|
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -388,7 +255,6 @@ class SpotubeTrack extends Track {
|
|||||||
"uri": uri,
|
"uri": uri,
|
||||||
"ytTrack": ytTrack.toJson(),
|
"ytTrack": ytTrack.toJson(),
|
||||||
"ytUri": ytUri,
|
"ytUri": ytUri,
|
||||||
"skipSegments": skipSegments,
|
|
||||||
"siblings": siblings.map((sibling) => sibling.toJson()).toList(),
|
"siblings": siblings.map((sibling) => sibling.toJson()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
// 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,
|
|
||||||
};
|
|
@ -296,14 +296,6 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
preferences.setPredownload(state);
|
preferences.setPredownload(state);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
|
||||||
secondary: const Icon(SpotubeIcons.fastForward),
|
|
||||||
title: Text(context.l10n.skip_non_music),
|
|
||||||
value: preferences.skipSponsorSegments,
|
|
||||||
onChanged: (state) {
|
|
||||||
preferences.setSkipSponsorSegments(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||||
title: Text(context.l10n.blacklist),
|
title: Text(context.l10n.blacklist),
|
||||||
|
@ -104,27 +104,6 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
|
|||||||
if (nextSource == null || isPlayable(nextSource)) return;
|
if (nextSource == null || isPlayable(nextSource)) return;
|
||||||
await audioPlayer.pause();
|
await audioPlayer.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.positionStream.listen((pos) async {
|
|
||||||
if (audioPlayer.currentIndex == -1) return;
|
|
||||||
final activeSource =
|
|
||||||
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex);
|
|
||||||
if (activeSource == null) return;
|
|
||||||
final activeTrack = state.tracks.firstWhereOrNull(
|
|
||||||
(element) => element is SpotubeTrack && element.ytUri == activeSource,
|
|
||||||
) as SpotubeTrack?;
|
|
||||||
if (activeTrack == null) return;
|
|
||||||
// skip all the activeTrack.skipSegments
|
|
||||||
if (activeTrack.skipSegments.isNotEmpty == true &&
|
|
||||||
preferences.skipSponsorSegments) {
|
|
||||||
for (final segment in activeTrack.skipSegments) {
|
|
||||||
if (pos.inSeconds < segment.start || pos.inSeconds >= segment.end) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await audioPlayer.seek(Duration(seconds: segment.end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
|
|
||||||
SpotubeColor accentColorScheme;
|
SpotubeColor accentColorScheme;
|
||||||
bool albumColorSync;
|
bool albumColorSync;
|
||||||
bool skipSponsorSegments;
|
|
||||||
|
|
||||||
String downloadLocation;
|
String downloadLocation;
|
||||||
|
|
||||||
@ -64,7 +63,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
this.saveTrackLyrics = false,
|
this.saveTrackLyrics = false,
|
||||||
this.checkUpdate = true,
|
this.checkUpdate = true,
|
||||||
this.audioQuality = AudioQuality.high,
|
this.audioQuality = AudioQuality.high,
|
||||||
this.skipSponsorSegments = true,
|
|
||||||
this.downloadLocation = "",
|
this.downloadLocation = "",
|
||||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||||
this.showSystemTrayIcon = true,
|
this.showSystemTrayIcon = true,
|
||||||
@ -132,12 +130,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
updatePersistence();
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSkipSponsorSegments(bool should) {
|
|
||||||
skipSponsorSegments = should;
|
|
||||||
notifyListeners();
|
|
||||||
updatePersistence();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDownloadLocation(String downloadDir) {
|
void setDownloadLocation(String downloadDir) {
|
||||||
if (downloadDir.isEmpty) return;
|
if (downloadDir.isEmpty) return;
|
||||||
downloadLocation = downloadDir;
|
downloadLocation = downloadDir;
|
||||||
@ -195,7 +187,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
audioQuality = map["audioQuality"] != null
|
audioQuality = map["audioQuality"] != null
|
||||||
? AudioQuality.values[map["audioQuality"]]
|
? AudioQuality.values[map["audioQuality"]]
|
||||||
: audioQuality;
|
: audioQuality;
|
||||||
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
|
|
||||||
downloadLocation =
|
downloadLocation =
|
||||||
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
|
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
|
||||||
|
|
||||||
@ -227,7 +218,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
"albumColorSync": albumColorSync,
|
"albumColorSync": albumColorSync,
|
||||||
"checkUpdate": checkUpdate,
|
"checkUpdate": checkUpdate,
|
||||||
"audioQuality": audioQuality.index,
|
"audioQuality": audioQuality.index,
|
||||||
"skipSponsorSegments": skipSponsorSegments,
|
|
||||||
"downloadLocation": downloadLocation,
|
"downloadLocation": downloadLocation,
|
||||||
"layoutMode": layoutMode.name,
|
"layoutMode": layoutMode.name,
|
||||||
"predownload": predownload,
|
"predownload": predownload,
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -152,7 +152,6 @@ abstract class TypeConversionUtils {
|
|||||||
),
|
),
|
||||||
file.path,
|
file.path,
|
||||||
[],
|
[],
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
track.album = Album()
|
track.album = Album()
|
||||||
..name = metadata?.album ?? "Spotube"
|
..name = metadata?.album ?? "Spotube"
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@ -1296,14 +1296,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
pocketbase:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: pocketbase
|
|
||||||
sha256: "125e32fe39393cc54436ce518ccfd5649419f15879a1278599f2f58564e5231c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.1+1"
|
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1945,4 +1937,4 @@ packages:
|
|||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.0.0 <4.0.0"
|
dart: ">=3.0.0 <4.0.0"
|
||||||
flutter: ">=3.3.0"
|
flutter: ">=3.10.0"
|
||||||
|
@ -71,7 +71,6 @@ dependencies:
|
|||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
permission_handler: ^10.2.0
|
permission_handler: ^10.2.0
|
||||||
pocketbase: ^0.7.1+1
|
|
||||||
popover: ^0.2.6+3
|
popover: ^0.2.6+3
|
||||||
queue: ^3.1.0+1
|
queue: ^3.1.0+1
|
||||||
scroll_to_index: ^3.0.1
|
scroll_to_index: ^3.0.1
|
||||||
|
Loading…
Reference in New Issue
Block a user