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:
|
||||
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
|
||||
SPOTIFY_SECRETS=
|
||||
|
@ -118,7 +118,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt
|
||||
|
||||
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
|
||||
- Debian (>=12/Bookworm)/Ubuntu
|
||||
```bash
|
||||
@ -137,7 +137,7 @@ Do the following:
|
||||
- Create a `.env` in root of the project following the `.env.example` template
|
||||
- Now run the following to bootstrap the project
|
||||
```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
|
||||
```bash
|
||||
|
@ -4,15 +4,6 @@ part 'env.g.dart';
|
||||
|
||||
@Envied(obfuscate: true, requireEnvFile: true, path: ".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')
|
||||
static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) {
|
||||
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_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.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/l10n/l10n.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/palette_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.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/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
@ -93,6 +94,13 @@ Future<void> main(List<String> rawArgs) async {
|
||||
cachePrefix: "oss.krtirtho.spotube",
|
||||
cacheDir: (await getApplicationSupportDirectory()).path,
|
||||
);
|
||||
Hive.registerAdapter(MatchedTrackAdapter());
|
||||
|
||||
await Hive.openLazyBox<MatchedTrack>(
|
||||
MatchedTrack.boxName,
|
||||
path: (await getApplicationSupportDirectory()).path,
|
||||
);
|
||||
|
||||
await PersistedStateNotifier.initializeBoxes();
|
||||
|
||||
Catcher(
|
||||
@ -164,7 +172,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
);
|
||||
},
|
||||
);
|
||||
await initializePocketBase();
|
||||
}
|
||||
|
||||
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:convert';
|
||||
|
||||
import 'package:catcher/catcher.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/piped.dart';
|
||||
import 'package:spotube/extensions/album_simple.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/models/track.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/pocketbase.dart';
|
||||
import 'package:spotube/services/youtube.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.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});
|
||||
|
||||
class SpotubeTrack extends Track {
|
||||
final PipedStreamResponse ytTrack;
|
||||
final String ytUri;
|
||||
final List<SkipSegment> skipSegments;
|
||||
|
||||
final List<PipedSearchItemStream> siblings;
|
||||
|
||||
SpotubeTrack(
|
||||
this.ytTrack,
|
||||
this.ytUri,
|
||||
this.skipSegments,
|
||||
this.siblings,
|
||||
) : super();
|
||||
|
||||
@ -47,7 +31,6 @@ class SpotubeTrack extends Track {
|
||||
required Track track,
|
||||
required this.ytTrack,
|
||||
required this.ytUri,
|
||||
required this.skipSegments,
|
||||
required this.siblings,
|
||||
}) : super() {
|
||||
album = track.album;
|
||||
@ -84,53 +67,7 @@ class SpotubeTrack extends Track {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<SkipSegment>> getSkipSegments(
|
||||
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 {
|
||||
static Future<List<PipedSearchItemStream>> fetchSiblings(Track track) async {
|
||||
final artists = (track.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
@ -143,69 +80,59 @@ class SpotubeTrack extends Track {
|
||||
onlyCleanArtist: true,
|
||||
).trim();
|
||||
|
||||
final cachedTracks = await Future<RecordModel?>.value(
|
||||
pb
|
||||
.collection(BackendTrack.collection)
|
||||
.getFirstListItem("spotify_id = '${track.id}'"),
|
||||
).catchError((e, stack) {
|
||||
return null;
|
||||
});
|
||||
final List<PipedSearchItemStream> siblings = await PipedSpotube.client
|
||||
.search(
|
||||
"$title - ${artists.join(", ")}",
|
||||
PipedFilter.musicSongs,
|
||||
)
|
||||
.then(
|
||||
(res) {
|
||||
final siblings = res.items
|
||||
.whereType<PipedSearchItemStream>()
|
||||
.where((item) {
|
||||
return artists.any(
|
||||
(artist) =>
|
||||
artist.toLowerCase() == item.uploaderName.toLowerCase(),
|
||||
);
|
||||
})
|
||||
.take(10)
|
||||
.toList();
|
||||
|
||||
final cachedTrack =
|
||||
cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null;
|
||||
if (siblings.isEmpty) {
|
||||
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;
|
||||
PipedAudioStream ytStream;
|
||||
List<PipedSearchItemStream> siblings = [];
|
||||
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);
|
||||
if (matchedCachedTrack != null) {
|
||||
ytVideo = await PipedSpotube.client.streams(matchedCachedTrack.youtubeId);
|
||||
} else {
|
||||
final videos = await PipedSpotube.client
|
||||
.search("${artists.join(", ")} - $title", PipedFilter.musicSongs);
|
||||
// await PrimitiveUtils.raceMultiple(
|
||||
// () => youtube.search.search("${artists.join(", ")} - $title"),
|
||||
// );
|
||||
siblings =
|
||||
videos.items.whereType<PipedSearchItemStream>().take(10).toList();
|
||||
final responses = await Future.wait(
|
||||
[
|
||||
PipedSpotube.client.streams(siblings.first.id),
|
||||
if (preferences.skipSponsorSegments)
|
||||
getSkipSegments(siblings.first.id, preferences)
|
||||
else
|
||||
Future.value([])
|
||||
],
|
||||
siblings = await fetchSiblings(track);
|
||||
ytVideo = await PipedSpotube.client.streams(siblings.first.id);
|
||||
|
||||
await MatchedTrack.box.put(
|
||||
track.id!,
|
||||
MatchedTrack(
|
||||
youtubeId: ytVideo.id,
|
||||
spotifyId: track.id!,
|
||||
),
|
||||
);
|
||||
ytVideo = responses.first as PipedStreamResponse;
|
||||
skipSegments = responses.last as List<SkipSegment>;
|
||||
ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
}
|
||||
|
||||
if (cachedTrack == null) {
|
||||
await Future<RecordModel?>.value(
|
||||
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;
|
||||
});
|
||||
}
|
||||
final PipedAudioStream ytStream =
|
||||
getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
|
||||
if (preferences.predownload &&
|
||||
ytVideo.duration < const Duration(minutes: 15)) {
|
||||
@ -230,7 +157,6 @@ class SpotubeTrack extends Track {
|
||||
track: track,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytStream.url,
|
||||
skipSegments: skipSegments,
|
||||
siblings: siblings,
|
||||
);
|
||||
}
|
||||
@ -241,59 +167,19 @@ class SpotubeTrack extends Track {
|
||||
) async {
|
||||
if (siblings.none((element) => element.id == video.id)) return null;
|
||||
|
||||
final [PipedStreamResponse ytVideo, List<SkipSegment> skipSegments] =
|
||||
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 ytVideo = await PipedSpotube.client.streams(video.id);
|
||||
|
||||
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
|
||||
final ytUri = ytStream.url;
|
||||
|
||||
final cachedTracks = await Future<RecordModel?>.value(
|
||||
pb.collection(BackendTrack.collection).getFirstListItem(
|
||||
"spotify_id = '$id' && youtube_id = '${video.id}'",
|
||||
),
|
||||
).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!,
|
||||
await MatchedTrack.box.put(
|
||||
id!,
|
||||
MatchedTrack(
|
||||
youtubeId: video.id,
|
||||
votes: 1,
|
||||
).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;
|
||||
});
|
||||
}
|
||||
spotifyId: id!,
|
||||
),
|
||||
);
|
||||
|
||||
if (preferences.predownload &&
|
||||
video.duration < const Duration(minutes: 15)) {
|
||||
@ -318,7 +204,6 @@ class SpotubeTrack extends Track {
|
||||
track: this,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytUri,
|
||||
skipSegments: skipSegments,
|
||||
siblings: [
|
||||
video,
|
||||
...siblings.where((element) => element.id != video.id),
|
||||
@ -331,7 +216,6 @@ class SpotubeTrack extends Track {
|
||||
track: Track.fromJson(map),
|
||||
ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
|
||||
ytUri: map["ytUri"],
|
||||
skipSegments: List.castFrom<dynamic, SkipSegment>(map["skipSegments"]),
|
||||
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
||||
.map((sibling) => PipedSearchItemStream.fromJson(sibling))
|
||||
.toList(),
|
||||
@ -340,30 +224,13 @@ class SpotubeTrack extends Track {
|
||||
|
||||
Future<SpotubeTrack> populatedCopy() async {
|
||||
if (this.siblings.isNotEmpty) return this;
|
||||
final artists = (this.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
|
||||
final title = ServiceUtils.getTitle(
|
||||
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();
|
||||
final siblings = await fetchSiblings(this);
|
||||
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: this,
|
||||
ytTrack: ytTrack,
|
||||
ytUri: ytUri,
|
||||
skipSegments: skipSegments,
|
||||
siblings: siblings,
|
||||
);
|
||||
}
|
||||
@ -388,7 +255,6 @@ class SpotubeTrack extends Track {
|
||||
"uri": uri,
|
||||
"ytTrack": ytTrack.toJson(),
|
||||
"ytUri": ytUri,
|
||||
"skipSegments": skipSegments,
|
||||
"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);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.fastForward),
|
||||
title: Text(context.l10n.skip_non_music),
|
||||
value: preferences.skipSponsorSegments,
|
||||
onChanged: (state) {
|
||||
preferences.setSkipSponsorSegments(state);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
title: Text(context.l10n.blacklist),
|
||||
|
@ -104,27 +104,6 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
|
||||
if (nextSource == null || isPlayable(nextSource)) return;
|
||||
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;
|
||||
bool albumColorSync;
|
||||
bool skipSponsorSegments;
|
||||
|
||||
String downloadLocation;
|
||||
|
||||
@ -64,7 +63,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
this.saveTrackLyrics = false,
|
||||
this.checkUpdate = true,
|
||||
this.audioQuality = AudioQuality.high,
|
||||
this.skipSponsorSegments = true,
|
||||
this.downloadLocation = "",
|
||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||
this.showSystemTrayIcon = true,
|
||||
@ -132,12 +130,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
updatePersistence();
|
||||
}
|
||||
|
||||
void setSkipSponsorSegments(bool should) {
|
||||
skipSponsorSegments = should;
|
||||
notifyListeners();
|
||||
updatePersistence();
|
||||
}
|
||||
|
||||
void setDownloadLocation(String downloadDir) {
|
||||
if (downloadDir.isEmpty) return;
|
||||
downloadLocation = downloadDir;
|
||||
@ -195,7 +187,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
audioQuality = map["audioQuality"] != null
|
||||
? AudioQuality.values[map["audioQuality"]]
|
||||
: audioQuality;
|
||||
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
|
||||
downloadLocation =
|
||||
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
|
||||
|
||||
@ -227,7 +218,6 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
"albumColorSync": albumColorSync,
|
||||
"checkUpdate": checkUpdate,
|
||||
"audioQuality": audioQuality.index,
|
||||
"skipSponsorSegments": skipSponsorSegments,
|
||||
"downloadLocation": downloadLocation,
|
||||
"layoutMode": layoutMode.name,
|
||||
"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,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
track.album = Album()
|
||||
..name = metadata?.album ?? "Spotube"
|
||||
|
10
pubspec.lock
10
pubspec.lock
@ -1296,14 +1296,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1945,4 +1937,4 @@ packages:
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
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_provider: ^2.0.8
|
||||
permission_handler: ^10.2.0
|
||||
pocketbase: ^0.7.1+1
|
||||
popover: ^0.2.6+3
|
||||
queue: ^3.1.0+1
|
||||
scroll_to_index: ^3.0.1
|
||||
|
Loading…
Reference in New Issue
Block a user