Compare commits

...

8 Commits

Author SHA1 Message Date
Kuldeep Singh
391bd607dd
Merge 7b85ab1617 into ba27dc70e4 2025-03-17 15:30:14 +01:00
Kingkor Roy Tirtho
ba27dc70e4
Merge pull request #2550 from KRTirtho/dev
Release 4.0.2
2025-03-16 23:57:54 +06:00
Kingkor Roy Tirtho
0ec9f3535b chore: bump to 4.0.2 and generate changelog 2025-03-16 23:52:08 +06:00
Kingkor Roy Tirtho
df72ba6960 chore: convert all spotify calls to invoke signature to capture invalid access token exception 2025-03-16 21:22:29 +06:00
Kingkor Roy Tirtho
d9057dae57 fix: invalid access token exception #2525 2025-03-16 10:32:41 +06:00
Kingkor Roy Tirtho
e61b79585e chore: remove print statement 2025-03-15 21:09:51 +06:00
Kingkor Roy Tirtho
a9e5636e96 chore: add a fallback init token retrieval method 2025-03-15 21:07:59 +06:00
Kuldeep Singh
7b85ab1617
Create app_pa.arb
Punjabi (pa) translation
2025-01-02 17:46:20 +05:30
39 changed files with 766 additions and 214 deletions

View File

@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [4.0.2](https://github.com/krtirtho/spotube/compare/v4.0.1...v4.0.2) (2025-03-16)
### Bug Fixes
- invalid access token exception #2525
## [4.0.1](https://github.com/krtirtho/spotube/compare/v4.0.0...v4.0.1) (2025-03-15)

View File

@ -30,7 +30,6 @@ import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -122,8 +121,9 @@ class TrackOptions extends HookConsumerWidget {
final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio";
final pages =
await spotify.search.get(query, types: [SearchType.playlist]).first();
final pages = await spotify.invoke(
(api) => api.search.get(query, types: [SearchType.playlist]).first(),
);
final radios = pages
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
@ -165,8 +165,9 @@ class TrackOptions extends HookConsumerWidget {
await playback.addTrack(track);
}
final tracks =
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
final tracks = await spotify.invoke(
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all(),
);
await playback.addTracks(
tracks.toList()

View File

@ -191,8 +191,7 @@ class TrackTile extends HookConsumerWidget {
const SizedBox(
width: 26,
height: 26,
child:
CircularProgressIndicator(size: 1.5),
child: CircularProgressIndicator(),
),
(_, _, true, _, _) => Icon(
SpotubeIcons.pause,

View File

@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart';
@ -27,7 +27,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
switch (url.pathSegments.first) {
case "album":
final album = await spotify.albums.get(url.pathSegments.last);
final album = await spotify.invoke((api) {
return api.albums.get(url.pathSegments.last);
});
router.navigate(
AlbumRoute(id: album.id!, album: album),
);
@ -36,7 +38,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
router.navigate(ArtistRoute(artistId: url.pathSegments.last));
break;
case "playlist":
final playlist = await spotify.playlists.get(url.pathSegments.last);
final playlist = await spotify.invoke((api) {
return api.playlists.get(url.pathSegments.last);
});
router
.navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
break;
@ -65,7 +69,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
switch (startSegment) {
case "spotify:album":
final album = await spotify.albums.get(endSegment);
final album = await spotify.invoke((api) {
return api.albums.get(endSegment);
});
await router.navigate(
AlbumRoute(id: album.id!, album: album),
);
@ -77,7 +83,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
await router.navigate(TrackRoute(trackId: endSegment));
break;
case "spotify:playlist":
final playlist = await spotify.playlists.get(endSegment);
final playlist = await spotify.invoke((api) {
return api.playlists.get(endSegment);
});
await router.navigate(
PlaylistRoute(id: playlist.id!, playlist: playlist),
);

View File

@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@ -28,8 +28,8 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last;
final query = "${track.name} Radio";
final pages = await spotify.search
.get(query, types: [SearchType.playlist]).first();
final pages = await spotify.invoke((api) =>
api.search.get(query, types: [SearchType.playlist]).first());
final radios = pages
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
@ -50,8 +50,8 @@ void useEndlessPlayback(WidgetRef ref) {
orElse: () => radios.first,
);
final tracks =
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
final tracks = await spotify.invoke(
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all());
await playback.addTracks(
tracks.toList()

405
lib/l10n/app_pa.arb Normal file
View File

@ -0,0 +1,405 @@
{
"guest": "ਮਹਿਮਾਨ",
"browse": "ਬ੍ਰਾਊਜ਼ ਕਰੋ",
"search": "ਖੋਜੋ",
"library": "ਲਾਇਬ੍ਰੇਰੀ",
"lyrics": "ਗੀਤਾਂ ਦੇ ਬੋਲ",
"settings": "ਸੈਟਿੰਗਾਂ",
"genre_categories_filter": "ਸ਼੍ਰੇਣੀਆਂ ਜਾਂ ਸ਼ੈਲੀਆਂ ਨੂੰ ਛਾਣੋ...",
"genre": "ਸ਼ੈਲੀ",
"personalized": "ਵਿਅਕਤੀਗਤ",
"featured": "ਵਿਸ਼ੇਸ਼ ਰੂਪ ਨਾਲ ਪ੍ਰਦਰਸ਼ਿਤ",
"new_releases": "ਨਵੀਂ ਰਿਲੀਜ਼",
"songs": "ਗੀਤ",
"playing_track": "{track} ਚੱਲ ਰਿਹਾ ਹੈ",
"queue_clear_alert": "ਇਹ ਮੌਜੂਦਾ ਕਤਾਰ ਨੂੰ ਸਾਫ਼ ਕਰ ਦੇਵੇਗਾ। {track_length} ਟ੍ਰੈਕ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
"load_more": "ਹੋਰ ਲੋਡ ਕਰੋ",
"playlists": "ਪਲੇਸੂਚੀ",
"artists": "ਕਲਾਕਾਰ",
"albums": "ਐਲਬਮ",
"tracks": "ਟ੍ਰੈਕ",
"downloads": "ਡਾਊਨਲੋਡ",
"filter_playlists": "ਆਪਣੀਆਂ ਪਲੇਸੂਚੀਆਂ ਨੂੰ ਛਾਣੋ...",
"liked_tracks": "ਪਸੰਦੀਦਾ ਟ੍ਰੈਕ",
"liked_tracks_description": "ਤੁਹਾਡੇ ਸਾਰੇ ਪਸੰਦੀਦਾ ਟ੍ਰੈਕ",
"create_playlist": "ਪਲੇਸੂਚੀ ਬਣਾਉ",
"create_a_playlist": "ਇੱਕ ਪਲੇਸੂਚੀ ਬਣਾਉ",
"create": "ਬਣਾਉ",
"cancel": "ਰੱਦ ਕਰੋ",
"playlist_name": "ਪਲੇਸੂਚੀ ਦਾ ਨਾਮ",
"name_of_playlist": "ਪਲੇਸੂਚੀ ਦਾ ਨਾਮ",
"description": "ਵੇਰਵਾ",
"public": "ਜਨਤਕ",
"collaborative": "ਸਹਿਯੋਗੀ",
"search_local_tracks": "ਸਥਾਨਕ ਟ੍ਰੈਕ ਖੋਜੋ...",
"play": "ਚਲਾਉ",
"delete": "ਹਟਾਉ",
"none": "ਕੋਈ ਨਹੀਂ",
"sort_a_z": "A-Z ਦਿਖਾਉ",
"sort_z_a": "Z-A ਦਿਖਾਉ",
"sort_artist": "ਕਲਾਕਾਰ ਅਨੁਸਾਰ ਦਿਖਾਉ",
"sort_album": "ਐਲਬਮ ਅਨੁਸਾਰ ਦਿਖਾਉ",
"sort_tracks": "ਟ੍ਰੈਕ ਅਨੁਸਾਰ ਦਿਖਾਉ",
"currently_downloading": "ਹੁਣ ਡਾਊਨਲੋਡ ਹੋ ਰਿਹਾ ਹੈ ({tracks_length})",
"cancel_all": "ਸਾਰਿਆਂ ਨੂੰ ਰੱਦ ਕਰੋ",
"filter_artist": "ਕਲਾਕਾਰ ਨੂੰ ਛਾਣੋ...",
"followers": "{followers} ਫ਼ਾਲੋਅਰ",
"add_artist_to_blacklist": "ਕਲਾਕਾਰ ਨੂੰ ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
"top_tracks": "ਚੋਟੀ ਦਾ ਟ੍ਰੈਕ",
"fans_also_like": "ਫੈਨਸ ਨੂੰ ਇਹ ਵੀ ਪਸੰਦ ਹੈ",
"loading": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
"artist": "ਕਲਾਕਾਰ",
"blacklisted": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਹੈ",
"following": "ਫ਼ਾਲੋ ਕਰ ਰਿਹਾ ਹੈ",
"follow": "ਫ਼ਾਲੋ ਕਰੋ",
"artist_url_copied": "ਕਲਾਕਾਰ ਯੂਆਰਐਲ ਕਲਿੱਪਬੋਰਡ ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
"added_to_queue": "{tracks} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਜੋੜੇ ਗਏ",
"filter_albums": "ਐਲਬਮਾਂ ਨੂੰ ਛਾਣੋ...",
"synced": "ਸਿੰਕ ਕੀਤਾ ਗਿਆ",
"plain": "ਸਾਦਾ",
"shuffle": "ਸ਼ਫਲ",
"search_tracks": "ਟ੍ਰੈਕ ਖੋਜੋ...",
"released": "ਜਾਰੀ ਹੋਇਆ",
"error": "ਤਰੁੱਟੀ {error}",
"title": "ਸਿਰਲੇਖ",
"time": "ਸਮਾਂ",
"more_actions": "ਹੋਰ ਕਾਰਵਾਈ",
"download_count": "ਡਾਊਨਲੋਡ ({count})",
"add_count_to_playlist": "({count}) ਨੂੰ ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
"add_count_to_queue": "({count}) ਨੂੰ ਕਤਾਰ ਵਿੱਚ ਜੋੜੋ",
"play_count_next": "({count}) ਅਗਲਾ ਚਲਾਉ",
"album": "ਐਲਬਮ",
"copied_to_clipboard": "{data} ਕਲਿੱਪਬੋਰਡ ਉੱਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
"add_to_following_playlists": "{track} ਨੂੰ ਹੇਠ ਲਿਖੀ ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
"add": "ਜੋੜੋ",
"added_track_to_queue": "{track} ਨੂੰ ਕਤਾਰ ਵਿੱਚ ਜੋੜਿਆ ਗਿਆ",
"add_to_queue": "ਕਤਾਰ ਵਿੱਚ ਜੋੜੋ",
"track_will_play_next": "{track} ਅਗਲਾ ਚਲਾਉ",
"play_next": "ਅਗਲਾ ਚਲਾਉ",
"removed_track_from_queue": "{track} ਨੂੰ ਕਤਾਰ ਵਿੱਚੋਂ ਹਟਾਇਆ ਗਿਆ",
"remove_from_queue": "ਕਤਾਰ ਵਿੱਚ ਹਟਾਉ",
"remove_from_favorites": "ਪਸੰਦੀਦਾ ਵਿੱਚੋਂ ਹਟਾਉ",
"save_as_favorite": "ਪਸੰਦੀਦਾ ਵਜੋਂ ਸੰਭਾਲੋ",
"add_to_playlist": "ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
"remove_from_playlist": "ਪਲੇਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਉ",
"add_to_blacklist": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
"remove_from_blacklist": "ਕਾਲੀਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਉ",
"share": "ਸਾਂਝਾ ਕਰੋ",
"mini_player": "ਮਿੰਨੀ ਪਲੇਅਰ",
"slide_to_seek": "ਅੱਗੇ ਜਾਂ ਪਿੱਛੇ ਖੋਜਣ ਲਈ ਸਲਾਈਡ ਕਰੋ",
"shuffle_playlist": "ਪਲੇਸੂਚੀ ਸ਼ਫਲ ਕਰੋ",
"unshuffle_playlist": "ਪਲੇਸੂਚੀ ਅਣਸ਼ਫਲ ਕਰੋ",
"previous_track": "ਪਿਛਲਾ ਟ੍ਰੈਕ",
"next_track": "ਅਗਲਾ ਟ੍ਰੈਕ",
"pause_playback": "ਪਲੇਬੈਕ ਰੋਕੋ",
"resume_playback": "ਪਲੇਬੈਕ ਚਲਾਉ",
"loop_track": "ਲੂਪ ਟ੍ਰੈਕ",
"repeat_playlist": "ਪਲੇਸੂਚੀ ਦੁਹਰਾਉ",
"queue": "ਕਤਾਰ",
"alternative_track_sources": "ਵਿਕਲਪਕ ਟ੍ਰੈਕ ਸਰੋਤ",
"download_track": "ਟ੍ਰੈਕ ਡਾਊਨਲੋਡ ਕਰੋ",
"tracks_in_queue": "{tracks} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਹਨ",
"clear_all": "ਸਾਰੇ ਹਟਾਉ",
"show_hide_ui_on_hover": "ਹਵਰ ਉੱਤੇ ਯੂਆਈ ਦਿਖਾਉ/ਛਿਪਾਉ",
"always_on_top": "ਹਮੇਸ਼ਾ ਉੱਤੇ ਹੋਵੇ",
"exit_mini_player": "ਮਿੰਨੀ ਪਲੇਅਰ ਤੋਂ ਬਾਹਰ ਨਿਕਲੋ",
"download_location": "ਡਾਊਨਲੋਡ ਸਥਾਨ",
"account": "ਖ਼ਾਤਾ",
"login_with_spotify": "ਆਪਣੇ ਸਪੋਟਫਾਈ ਖ਼ਾਤੇ ਨਾਲ ਦਾਖ਼ਲ ਹੋਵੋ",
"connect_with_spotify": "ਸਪੋਟਫਾਈ ਨਾਲ ਕਨੈਕਟ ਕਰੋ",
"logout": "ਵਿਦਾਈ ਲਵੋ",
"logout_of_this_account": "ਇਸ ਖ਼ਾਤੇ ਤੋਂ ਵਿਦਾਈ ਲਵੋ",
"language_region": "ਭਾਸ਼ਾ ਅਤੇ ਖੇਤਰ",
"language": "ਭਾਸ਼ਾ",
"system_default": "ਸਿਸਟਮ ਡਿਫਾਲਟ",
"market_place_region": "ਮਾਰਕਿਟਪਲੇਸ ਖੇਤਰ",
"recommendation_country": "ਸੁਝਾਇਆ ਗਿਆ ਦੇਸ਼",
"appearance": "ਦਿੱਖ",
"layout_mode": "ਲੇਆਊਟ ਮੋਡ",
"override_layout_settings": "ਓਵਰਰਾਈਡ ਰਿਸਪਾਂਸਿਵ ਲੇਆਊਟ ਮੋਡ ਸੈਟਿੰਗ",
"adaptive": "ਅਨੁਕੂਲ",
"compact": "ਕੰਪੈਕਟ",
"extended": "ਵਿਸਤ੍ਰਿਤ",
"theme": "ਥੀਮ",
"dark": "ਗੂੜ੍ਹਾ",
"light": "ਚਾਨਣ",
"system": "ਸਿਸਟਮ",
"accent_color": "ਅੱਖਰਸ਼ੈਲੀ ਦਾ ਰੰਗ",
"sync_album_color": "ਐਲਬਮ ਦਾ ਰੰਗ ਸਿੰਕ ਕਰੋ",
"sync_album_color_description": "ਐਲਬਮ ਕਲਾ ਦਾ ਪ੍ਰਾਇਮਰੀ ਰੰਗ ਇੱਕ ਲਹਿਜ਼ੇ ਦੇ ਰੰਗ ਵਜੋਂ ਵਰਤਿਆ ਜਾਂਦਾ ਹੈ",
"playback": "ਪਲੇਬੈਕ",
"audio_quality": "ਆਡੀਉ ਗੁਣਵੱਤਾ",
"high": "ਉੱਚ",
"low": "ਨਿਮਨ",
"pre_download_play": "ਪਹਿਲਾਂ ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਚਲਾਉ",
"pre_download_play_description": "ਸਟ੍ਰੀਮਿੰਗ ਆਡੀਓ ਦੀ ਬਜਾਏ ਬਾਈਟ ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਚਲਾਓ (ਉੱਚ ਬੈਂਡਵਿਡਥ ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਸਿਫ਼ਾਰਿਸ਼ ਕੀਤੀ ਗਈ ਹੈ)",
"skip_non_music": "ਗੀਤ ਦੇ ਇਲਾਵਾ ਸੈਗਮੈਂਟਾਂ ਨੂੰ ਛੱਡੋ (ਸਪਾਂਸਰਬਲਾਕ)",
"blacklist_description": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਸ਼ਾਮਲ ਟ੍ਰੈਕ ਅਤੇ ਕਲਾਕਾਰ",
"wait_for_download_to_finish": "ਵਰਤਮਾਨ ਡਾਊਨਲੋਡ ਸਮਾਪਤ ਹੋਣ ਤੱਕ ਇੰਤਜ਼ਾਰ ਕਰੋ",
"desktop": "ਡੈਸਕਟਾਪ",
"close_behavior": "ਬੰਦ ਕਰਨ ਦਾ ਵਿਵਹਾਰ",
"close": "ਬੰਦ ਕਰੋ",
"minimize_to_tray": "ਟ੍ਰੇ ਵਿੱਚ ਘੱਟ ਕਰੋ",
"show_tray_icon": "ਸਿਸਟਮ ਟ੍ਰੇ ਆਈਕਨ ਦਿਖਾਉ",
"about": "ਬਾਬਤ",
"u_love_spotube": "ਅਸੀਂ ਜਾਣਦੇ ਹਾਂ ਕਿ ਤੁਸੀਂ Spotube ਨੂੰ ਪਿਆਰ ਕਰਦੇ ਹੋ",
"check_for_updates": "ਅੱਪਡੇਟ ਲਈ ਜਾਂਚ ਕਰੋ",
"about_spotube": "Spotube ਦੇ ਬਾਰੇ",
"blacklist": "ਕਾਲੀਸੂਚੀ",
"please_sponsor": "ਕਿਰਪਾ ਕਰਕੇ ਸਪਾਂਸਰ / ਦਾਨ ਕਰੋ",
"spotube_description": "Spotube, ਇੱਕ ਹਲਕਾ, ਸਾਰੇ ਪਲੇਟਫਾਰਮਾਂ ਤੇ ਚੱਲਣ ਵਾਲਾ, ਮੁਫ਼ਤ ਸਪੋਟੀਫਾਈ ਕਲਾਇੰਟ",
"version": "ਸੰਸਕਰਨ",
"build_number": "ਬਿਲਡ ਨੰਬਰ",
"founder": "ਸੰਸਥਾਪਕ",
"repository": "ਭੰਡਾਰ",
"bug_issues": "ਦਿੱਕਤਾਂ+ਮੁੱਦੇ",
"made_with": "ਬੰਗਲਾਦੇਸ਼🇧🇩 ਵਿੱਚ ਦਿਲ ਨਾਲ ਬਣਾਇਆ ਗਿਆ",
"kingkor_roy_tirtho": "ਕਿੰਗਕੋਰ ਰੌਏ ਤਿਰਥੋ",
"copyright": "© 2021-{current_year} ਕਿੰਗਕੋਰ ਰੌਏ ਤਿਰਥੋ",
"license": "ਲਸੰਸ",
"add_spotify_credentials": "ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਤੁਹਾਡੇ ਸਪੋਟੀਫਾਈ ਕਰੇਡੈਂਸ਼ੀਅਲ ਜੋੜੋ",
"credentials_will_not_be_shared_disclaimer": "ਚਿੰਤਾ ਨਾ ਕਰੋ, ਤੁਹਾਡੇ ਕਰੇਡੈਂਸ਼ੀਅਲ ਕਿਸੇ ਵੀ ਤਰ੍ਹਾਂ ਨਾਲ ਇਕੱਠੇ ਜਾਂ ਸਾਂਝੇ ਨਹੀਂ ਕੀਤੇ ਜਾਣਗੇ",
"know_how_to_login": "ਇਸਨੂੰ ਕਿਵੇਂ ਕਰਨਾ ਹੈ, ਨਹੀਂ ਪਤਾ?",
"follow_step_by_step_guide": "ਕਦਮ ਨਾਲ ਕਦਮ ਗਾਈਡ ਦੇ ਨਾਲ ਚੱਲੋ",
"spotify_cookie": "ਸਪੋਟੀਫਾਈ {name} ਕੁਕੀ",
"cookie_name_cookie": "{name} ਕੁਕੀ",
"fill_in_all_fields": "ਕ੍ਰਿਪਾ ਸਾਰੇ ਫੀਲਡ ਭਰੋ",
"submit": "ਸਪੁਰਦ",
"exit": "ਬਾਹਰ ਨਿਕਲੋ",
"previous": "ਪਿਛਲਾ",
"next": "ਅਗਲਾ",
"done": "ਹੋ ਗਿਆ",
"step_1": "ਚਰਣ 1",
"first_go_to": "ਪਹਿਲਾਂ, ਜਾਉ",
"login_if_not_logged_in": "ਅਤੇ ਜੇਕਰ ਤੁਸੀਂ ਦਾਖ਼ਲ ਨਹੀਂ ਹੋਏ ਹੋ ਤਾਂ ਦਾਖ਼ਲ ਹੋਵੋ / ਸਾਈਨਅੱਪ ਕਰੋ",
"step_2": "ਚਰਣ 2",
"step_2_steps": "1. ਇੱਕ ਵਾਰ ਜਦੋਂ ਤੁਸੀਂ ਦਾਖ਼ਲ ਹੋ ਜਾਂਦੇ ਹੋ, ਤਾਂ F12 ਦਬਾਓ ਜਾਂ ਮਾਊਸ ਸੱਜਾ ਕਲਿੱਕ ਕਰੋ > ਜਾਂਚ ਕਰੋ ਤਾਂ ਕਿ ਬ੍ਰਾਊਜ਼ਰ DevTools ਖੁੱਲ੍ਹ ਜਾਵੇ।\n2. ਫਿਰ ਬ੍ਰਾਊਜ਼ਰ ਦੇ \"ਐਪਲੀਕੇਸ਼ਨ\" ਟੈਬ (Chrome, Edge, Brave ਆਦਿ) ਜਾਂ \"ਸਟੋਰੇਜ\" ਟੈਬ (Firefox, Palemoon ਆਦਿ) ਵਿੱਚ ਜਾਉ\n3. \"ਕੁਕੀਜ\" ਅਨੁਭਾਗ ਵਿੱਚ ਜਾਉ ਫਿਰ \"https: //accounts.spotify.com\" ਉਪ-ਅਨੁਭਾਗ ਵਿੱਚ ਜਾਉ",
"step_3": "ਚਰਣ 3",
"success_emoji": "ਸਫ਼ਲ🥳",
"success_message": "ਤੁਸੀਂ ਹੁਣ ਆਪਣੇ Spotify ਖ਼ਾਤੇ ਨਾਲ ਸਫ਼ਲਤਾਪੂਰਵਕ ਦਾਖ਼ਲ ਹੋ ਗਏ ਹੋ। ਤੁਸੀਂ ਵਧੀਆ ਕੰਮ ਕੀਤਾ!",
"step_4": "ਚਰਣ 4",
"something_went_wrong": "ਕੁੱਝ ਗਲਤ ਹੋ ਗਿਆ ਹੈ",
"piped_instance": "ਪਾਈਪਡ ਸਰਵਰ",
"piped_description": "ਪਾਈਪ ਕੀਤੇ ਗਏ ਸਰਵਰ",
"piped_warning": "ਗਾਣਿਆਂ ਨਾਲ ਮੇਲ ਕਰਨ ਲਈ ਵਰਤੇ ਜਾਂਦੇ ਹਨ, ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਉਹਨਾਂ ਵਿੱਚੋਂ ਕੁਝ ਦੇ ਨਾਲ ਸਹੀ ਢੰਗ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ ਇਸਲਈ ਆਪਣੇ ਜੋਖਮ 'ਤੇ ਵਰਤੋਂ",
"generate_playlist": "ਪਲੇਸੂਚੀ ਬਣਾਉ",
"track_exists": "ਟ੍ਰੈਕ {track} ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ",
"replace_downloaded_tracks": "ਸਾਰੇ ਡਾਊਨਲੋਡ ਕੀਤੇ ਗਏ ਟ੍ਰੈਕ ਬਦਲੋ",
"skip_download_tracks": "ਸਾਰੇ ਡਾਊਨਲੋਡ ਕੀਤੇ ਗਏ ਟ੍ਰੈਕ ਛੱਡੋ",
"do_you_want_to_replace": "ਕੀ ਤੁਸੀਂ ਮੌਜੂਦਾ ਟ੍ਰੈਕ ਬਦਲਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
"replace": "ਬਦਲੋ",
"skip": "ਛੱਡੋ",
"select_up_to_count_type": "{count} {type} ਤੱਕ ਚੁਣੋ",
"select_genres": "ਸ਼ੈਲੀਆਂ ਚੁਣੋ",
"add_genres": "ਸ਼ੈਲੀਆਂ ਜੋੜੋ",
"country": "ਦੇਸ਼",
"number_of_tracks_generate": "ਉਤਪੰਨ ਕਰਨ ਲਈ ਟ੍ਰੈਕਾਂ ਦੀ ਸੰਖਿਆ",
"acousticness": "ਧੁਨਿਕਤਾ",
"danceability": "ਨੱਚਣ ਵਾਲੇ",
"energy": "ਊਰਜਾ",
"instrumentalness": "ਅਲਾਪਿਕਤਾ",
"liveness": "ਜੀਵੰਤਤਾ",
"loudness": "ਸ਼ੋਰ",
"speechiness": "ਬੋਲਚਾਲਤਾ",
"valence": "ਮਨੋਦਯਤਾ",
"popularity": "ਲੋਕਪ੍ਰਿਯਤਾ",
"key": "ਕੁੰਜੀ",
"duration": "ਅਵਧੀ (ਸੈਕਿੰਡ)",
"tempo": "ਗਤੀ (BPM)",
"mode": "ਮੋਡ",
"time_signature": "ਸਮਾਂ ਛਾਪ",
"short": "ਸੰਖੇਪ",
"medium": "ਮੱਧਮ",
"long": "ਲੰਬਾ",
"min": "ਨਿਊਨਤਮ",
"max": "ਅਧਿਕਤਮ",
"target": "ਟੀਚਾ",
"moderate": "ਮੱਧਮ",
"deselect_all": "ਸਭ ਨੂੰ ਅਣਚੁਣਿਆ ਕਰੋ",
"select_all": "ਸਭ ਨੂੰ ਚੁਣੋ",
"are_you_sure": "ਕੁ ਤੁਹਾਨੂੰ ਯਕੀਨ ਹੈ?",
"generating_playlist": "ਤੁਹਾਡੀ ਆਪਣੀ ਪਲੇਸੂਚੀ ਬਣਾਈ ਜਾ ਰਹੀ ਹੈ...",
"selected_count_tracks": "{count} ਟ੍ਰੈਕ ਚੁਣੇ ਗਏ",
"download_warning": "ਜੇਕਰ ਤੁਸੀਂ ਸਾਰੇ ਟਰੈਕਾਂ ਨੂੰ ਬਲਕ ਵਿੱਚ ਡਾਊਨਲੋਡ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਸਪਸ਼ਟ ਤੌਰ 'ਤੇ ਸੰਗੀਤ ਦੀ ਗੈਰ-ਕਾਨੂੰਨੀ ਨਕਲ ਕਰ ਰਹੇ ਹੋ ਅਤੇ ਸੰਗੀਤ ਦੇ ਰਚਨਾਤਮਕ ਸਮਾਜ ਨੂੰ ਨੁਕਸਾਨ ਪਹੁੰਚਾ ਰਹੇ ਹੋ। ਮੈਨੂੰ ਉਮੀਦ ਹੈ ਕਿ ਤੁਸੀਂ ਇਸ ਬਾਰੇ ਜਾਣਦੇ ਹੋ। ਹਮੇਸ਼ਾ ਕਲਾਕਾਰ ਦੀ ਮਿਹਨਤ ਦਾ ਸਤਿਕਾਰ ਅਤੇ ਸਮਰਥਨ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
"download_ip_ban_warning": "ਬਾਹਰੀ ਡਾਉਨਲੋਡ ਬੇਨਤੀਆਂ ਕਾਰਨ ਤੁਹਾਡਾ IP YouTube 'ਤੇ ਵੱਧ ਤੋਂ ਵੱਧ ਬਲੌਕ ਹੋ ਸਕਦਾ ਹੈ। IP ਬਲਾਕ ਦਾ ਮਤਲਬ ਹੈ ਕਿ ਤੁਸੀਂ ਘੱਟੋ-ਘੱਟ 2-3 ਮਹੀਨਿਆਂ ਲਈ ਉਸੇ IP ਡਿਵਾਈਸ ਤੋਂ YouTube ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਕਰ ਸਕੋਗੇ (ਭਾਵੇਂ ਤੁਸੀਂ ਲੌਗਇਨ ਕੀਤਾ ਹੋਵੇ)। ਅਤੇ ਜੇਕਰ ਅਜਿਹਾ ਹੁੰਦਾ ਹੈ ਤਾਂ Spottube ਕੋਈ ਜ਼ਿੰਮੇਵਾਰੀ ਨਹੀਂ ਲੈਂਦਾ।",
"by_clicking_accept_terms": "'ਸਵੀਕਾਰ' ਕਲਿੱਕ ਕਰਕੇ ਤੁਸੀਂ ਹੇਠਾਂ ਦਿੱਤੀਆਂ ਸ਼ਰਤਾਂ ਨਾਲ ਸਹਿਮਤ ਹੁੰਦੇ ਹੋ:",
"download_agreement_1": "ਮੈਨੂੰ ਪਤਾ ਹੈ ਕਿ ਮੈਂ ਗ਼ੈਰ-ਕਾਨੂੰਨੀ ਸੰਗੀਤ ਨੂੰ ਨਕਲੀ ਬਣਾ ਰਿਹਾ ਹਾਂ। ਮੈਂ ਬੁਰਾ ਹਾਂ",
"download_agreement_2": "ਮੈਂ ਜਿੱਥੇ ਵੀ ਹੋ ਸਕੇ ਕਲਾਕਾਰਾਂ ਦਾ ਸਮਰਥਨ ਕਰਾਂਗਾ ਅਤੇ ਮੈਂ ਅਜਿਹਾ ਇਸ ਲਈ ਕਰ ਰਿਹਾ ਹਾਂ ਕਿਉਂਕਿ ਮੇਰੇ ਕੋਲ ਉਨ੍ਹਾਂ ਦੀ ਕਲਾ ਖਰੀਦਣ ਲਈ ਪੈਸੇ ਨਹੀਂ ਹਨ।",
"download_agreement_3": "ਮੈਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਪਤਾ ਹੈ ਕਿ ਮੇਰਾ ਆਈਪੀ ਯੂਟਿਊਬ 'ਤੇ ਬਲੌਕ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ ਅਤੇ ਮੈਂ ਕਿਸੇ ਵੀ ਦੁਰਘਟਨਾ ਲਈ ਸਪੋਟੀਫਾਈ ਜਾਂ ਇਸਦੇ ਮਾਲਕਾਂ/ਸਬੰਧੀਆਂ ਨੂੰ ਜ਼ਿੰਮੇਵਾਰ ਨਹੀਂ ਠਹਿਰਾਉਂਦਾ ਹਾਂ।",
"decline": "ਇਨਕਾਰ ਕਰੋ",
"accept": "ਸਵੀਕਾਰ ਕਰੋ",
"details": "ਵਿਵਰਣ",
"youtube": "ਯੂਟਿਊਬ",
"channel": "ਚੈਨਲ",
"likes": "ਪਸੰਦ",
"dislikes": "ਨਾਪਸੰਦ",
"views": "ਵਿਯੂ",
"streamUrl": "ਸਟ੍ਰੀਮ ਯੂਆਰਐਲ",
"stop": "ਰੋਕੋ",
"sort_newest": "ਨਵੇਂ ਜੋੜੇ ਗਏ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
"sort_oldest": "ਸਭਤੋਂ ਪੁਰਾਣੇ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
"sleep_timer": "ਸਲੀਪ ਟਾਈਮਰ",
"mins": "{minutes} ਮਿੰਟ",
"hours": "{hours} ਘੰਟੇ",
"hour": "{hours} ਘੰਟਾ",
"custom_hours": "ਕਸਟਮ ਘੰਟੇ",
"logs": "ਚਿੱਠੇ",
"developers": "ਡਿਵੈਲਪਰ",
"not_logged_in": "ਤੁਸੀਂ ਦਾਖ਼ਲ ਨਹੀਂ ਹੋ",
"search_mode": "ਖੋਜ ਮੋਡ",
"audio_source": "ਆਡੀਓ ਸਰੋਤ",
"ok": "ਠੀਕ ਹੈ",
"failed_to_encrypt": "ਇਨਕ੍ਰਿਪਟ ਕਰਨ ਵਿੱਚ ਅਸਫਲ",
"encryption_failed_warning": "Spottube ਤੁਹਾਡੇ ਡੇਟਾ ਨੂੰ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਸਟੋਰ ਕਰਨ ਲਈ ਐਨਕ੍ਰਿਪਸ਼ਨ ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਪਰ ਇਹ ਅਸਫਲ ਰਿਹਾ. ਇਸ ਲਈ, ਇਹ ਅਸੁਰੱਖਿਅਤ ਸਟੋਰੇਜ 'ਤੇ ਵਾਪਸ ਆ ਜਾਵੇਗਾ\nਜੇਕਰ ਤੁਸੀਂ ਲੀਨਕਸ ਦੀ ਵਰਤੋਂ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ ਕਿਰਪਾ ਕਰਕੇ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡੇ ਕੋਲ ਕੋਈ ਗੁਪਤ-ਸੇਵਾਵਾਂ ਜਿਵੇਂ ਕਿ gnome-keyring, kde-wallet, Keepassxc ਆਦਿ ਇੰਸਟਾਲ ਹਨ।",
"querying_info": "ਜਾਣਕਾਰੀ ਪ੍ਰਾਪਤ ਕਰ ਰਿਹਾ ਹੈ",
"piped_api_down": "ਪਾਈਪਡ ਏਪੀਆਈ ਡਾਊਨ ਹੈ",
"piped_down_error_instructions": "ਪਾਈਪਡ ਇੰਸਟੈਂਸ {pipedInstance} ਵਰਤਮਾਨ ਵਿੱਚ ਡਾਊਨ ਹੈ\n\nਇੰਸਟੈਂਸ ਬਦਲੋ ਜਾਂ 'ਏਪੀਆਈ ਪ੍ਰਕਾਰ' ਨੂੰ ਅਧਿਕਾਰਤ YouTube API ਵਿੱਚ ਬਦਲੋ\n\nਤਬਦੀਲੀਆਂ ਤੋਂ ਬਾਅਦ ਐਪ ਨੂੰ ਦੁਬਾਰਾ ਚਲਾਉਣਾ ਯਕੀਨੀ ਬਣਾਓ",
"you_are_offline": "ਤੁਸੀਂ ਵਰਤਮਾਨ ਵਿੱਚ ਆਫ਼ਲਾਈਨ ਹੋ",
"connection_restored": "ਤੁਹਾਡਾ ਇੰਟਰਨੈੱਟ ਕੁਨੈਕਸ਼ਨ ਬਹਾਲ ਹੋ ਗਿਆ ਹੈ",
"use_system_title_bar": "ਸਿਸਟਮ ਸਿਰਲੇਖ ਪੱਟੀ ਵਰਤੋ",
"update_playlist": "ਪਲੇਸੂਚੀ ਅੱਪਡੇਟ ਕਰੋ",
"update": "ਅੱਪਡੇਟ ਕਰੋ",
"crunching_results": "ਨਤੀਜਿਆਂ 'ਤੇ ਕਾਰਵਾਈ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ...",
"search_to_get_results": "ਨਤੀਜੇ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਖੋਜੋ",
"use_amoled_mode": "AMOLED ਮੋਡ ਵਰਤੋ",
"pitch_dark_theme": "ਪਿੱਚ ਬਲੈਕ ਗੂੜ੍ਹੀ ਥੀਮ",
"normalize_audio": "ਆਡੀਓ ਨੂੰ ਸਧਾਰਨ ਕਰੋ",
"change_cover": "ਕਵਰ ਬਦਲੋ",
"add_cover": "ਕਵਰ ਜੋੜੋ",
"restore_defaults": "ਡਿਫਾਲਟ ਸੈਟਿੰਗਾਂ ਬਹਾਲ ਕਰੋ",
"download_music_codec": "ਸੰਗੀਤ ਕੋਡੈਕ ਡਾਊਨਲੋਡ ਕਰੋ",
"streaming_music_codec": "ਸਟ੍ਰੀਮਿੰਗ ਸੰਗੀਤ ਕੋਡੈਕ",
"login_with_lastfm": "Last.fm ਨਾਲ ਦਾਖ਼ਲ ਹੋਵੋ",
"connect": "ਕਨੈਕਟ ਕਰੋ",
"disconnect_lastfm": "Last.fm ਤੋਂ ਡਿਸਕਨੈਕਟ ਕਰੋ",
"disconnect": "ਡਿਸਕਨੈਕਟ ਕਰੋ",
"username": "ਉਪਯੋਗਕਰਤਾ ਨਾਮ",
"password": "ਪਾਸਵਰਡ",
"login": "ਦਾਖ਼ਲ ਹੋਵੋ",
"login_with_your_lastfm": "ਆਪਣੇ Last.fm ਖ਼ਾਤੇ ਨਾਲ਼ ਦਾਖ਼ਲ ਹੋਵੋ",
"scrobble_to_lastfm": "Last.fm ਉੱਤੇ ਸਕਰੌਬਲ ਕਰੋ",
"go_to_album": "ਐਲਬਮ ਤੇ ਜਾਓ",
"discord_rich_presence": "ਡਿਸਕਾਰਡ ਰਿਚ ਪ੍ਰੈਜੈਂਸ",
"browse_all": "ਸਾਰਿਆਂ ਨੂੰ ਬ੍ਰਾਊਜ਼ ਕਰੋ",
"genres": "ਸ਼ੈਲੀਆਂ",
"explore_genres": "ਸ਼ੈਲੀਆਂ ਫਰੋਲੋ",
"step_3_steps": "\"sp_dc\" ਕੁਕੀ ਦਾ ਮੁੱਲ ਕਾਪੀ ਕਰੋ",
"step_4_steps": "ਕਾਪੀ ਕੀਤੇ ਗਏ \"sp_dc\" ਮੁੱਲ ਨੂੰ ਪੈਸਟ ਕਰੋ",
"friends": "ਦੋਸਤ",
"no_lyrics_available": "ਮਾਫ਼ ਕਰਨਾ, ਇਸ ਟ੍ਰੈਕ ਦੇ ਬੋਲ ਨਹੀਂ ਮਿਲ ਸਕੋ",
"sort_duration": "ਅਵਧੀ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
"start_a_radio": "ਰੇਡੀਉ ਸ਼ੁਰੂ ਕਰੋ",
"how_to_start_radio": "ਰੇਡੀਉ ਕਿਵੇਂ ਸ਼ੁਰੂ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"replace_queue_question": "ਕੀ ਤੁਸੀਂ ਮੌਜੂਦਾ ਕਤਾਰ ਨੂੰ ਬਦਲਣਾ ਚਾਹੁੰਦੇ ਹੋ ਜਾਂ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"endless_playback": "ਬੇਅੰਤ ਪਲੇਬੈਕ",
"delete_playlist": "ਪਲੇਸੂਚੀ ਹਟਾਉ",
"delete_playlist_confirmation": "ਕੀ ਤੁਸੀਂ ਸੱਚਮੁੱਚ ਇਸ ਪਲੇਸੂਚੀ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
"local_tracks": "ਸਥਾਨਕ ਟ੍ਰੈਕ",
"song_link": "ਗੀਤ ਦਾ ਲਿੰਕ",
"skip_this_nonsense": "ਇਸਨੂੰ ਛੱਡੋ",
"freedom_of_music": "“ਸੰਗੀਤ ਦੀ ਅਜ਼ਾਦੀ”",
"freedom_of_music_palm": "“ਹੱਥ ਵਿੱਚ ਸੰਗੀਤ ਦੀ ਅਜ਼ਾਦੀ”",
"get_started": "ਆਉ ਸ਼ੁਰੂ ਕਰੋ",
"youtube_source_description": "ਸਿਫ਼ਾਰਸ਼ ਕੀਤਾ ਗਿਆ ਅਤੇ ਸਭਤੋਂ ਵਧੀਆ ਕੰਮ ਕਰਦਾ ਹੈ।",
"piped_source_description": "ਮੁਫ਼ਤ ਮਹਿਸੂਸ ਕਰ ਰਹੇ ਹੋ? YouTube ਦੇ ਸਮਾਨ ਪਰ ਕਾਫੀ ਮੁਫ਼ਤ।",
"jiosaavn_source_description": "ਦੱਖਣੀ ਏਸ਼ੀਆਈ ਖੇਤਰ ਲਈ ਸਭ ਤੋਂ ਵਧੀਆ।",
"highest_quality": "ਸਭਤੋਂ ਵਧੀਆ ਗੁਣਵੱਤਾ: {quality}",
"select_audio_source": "ਆਡੀਉ ਸਰੋਤ ਚੁਣੋ",
"endless_playback_description": "ਕ੍ਰਮਬੱਧ ਕਤਾਰ ਦੇ ਅੰਤ ਵਿੱਚ ਆਪਣੇ ਆਪ ਨਵੇਂ ਗੀਤ ਸ਼ਾਮਲ ਕਰੋ",
"choose_your_region": "ਆਪਣਾ ਖੇਤਰ ਚੁਣੋ",
"choose_your_region_description": "ਇਹ Spotube ਨੂੰ ਤੁਹਾਡੇ ਟਿਕਾਣੇ ਲਈ ਸਹੀ ਸਮੱਗਰੀ ਦਿਖਾਉਣ ਵਿੱਚ ਤੁਹਾਡੀ ਮਦਦ ਕਰੇਗਾ।",
"choose_your_language": "ਆਪਣੀ ਭਾਸ਼ਾ ਚੁਣੋ",
"help_project_grow": "ਇਸ ਪਰਿਯੋਜਨਾ ਨੂੰ ਵਧਾਉਣ ਵਿੱਚ ਮਦਦ ਕਰੋ",
"help_project_grow_description": "Spotube ਇੱਕ ਓਪਨ ਸੋਰਸ ਪ੍ਰੋਜੈਕਟ ਹੈ। ਤੁਸੀਂ ਯੋਗਦਾਨ ਦੇ ਕੇ, ਬੱਗਾਂ ਦੀ ਰਿਪੋਰਟ ਕਰਕੇ, ਜਾਂ ਨਵੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਦਾ ਸੁਝਾਅ ਦੇ ਕੇ ਇਸ ਪ੍ਰੋਜੈਕਟ ਨੂੰ ਵਧਾ ਸਕਦੇ ਹੋ।",
"contribute_on_github": "GitHub ਉੱਤੇ ਯੋਗਦਾਨ ਦਿਉ",
"donate_on_open_collective": "ਓਪਨ ਕਲੈਕਟਿਵ ਉੱਤੇ ਯੋਗਦਾਨ ਦਿਉ",
"browse_anonymously": "ਬਿਨ੍ਹਾ ਨਾਮ ਦੇ ਬ੍ਰਾਊਜ਼ ਕਰੋ",
"enable_connect": "ਕਨੈਕਟ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ",
"enable_connect_description": "ਹੋਰ ਡਿਵਾਈਸਾਂ ਤੋਂ Spottube ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ",
"devices": "ਉਪਕਰਣ",
"select": "ਚੁਣੋ",
"connect_client_alert": "ਤੁਹਾਨੂੰ {client} ਦੁਆਰਾ ਕੰਟਰੋਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
"this_device": "ਇਹ ਉਪਕਰਣ",
"remote": "ਰਿਮੋਟ",
"local_library": "ਸਥਾਨਕ ਲਾਇਬ੍ਰੇਰੀ",
"add_library_location": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਜੋੜੋ",
"remove_library_location": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ ਹਟਾਉ",
"local_tab": "ਸਥਾਨਕ",
"stats": "ਅੰਕੜੇ",
"and_n_more": "और {count} और",
"recently_played": "ਹਾਲ ਹੀ ਵਿੱਚ ਚਲਾਏ ਗਏ",
"browse_more": "ਹੋਰ ਬ੍ਰਾਉਜ਼ ਕਰੋ",
"no_title": "ਕੋਈ ਸਿਰਲੇਖ ਨਹੀਂ",
"not_playing": "ਚੱਲ ਨਹੀਂ ਰਿਹਾ",
"epic_failure": "ਮਹਾਨ ਅਸਫ਼ਲਤਾ!",
"added_num_tracks_to_queue": "{tracks_length} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਜੋੜੇ ਗਏ",
"spotube_has_an_update": "Spotube ਲਈ ਇੱਕ ਅੱਪਡੇਟ ਹੈ",
"download_now": "ਹੁਣੇ ਡਾਊਨਲੋਡ ਕਰੋ",
"nightly_version": "Spotube Nightly {nightlyBuildNum} ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ",
"release_version": "Spotube v{version} ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ",
"read_the_latest": "ਨਵੀਨਤਮ ਪੜ੍ਹੋ",
"release_notes": "ਰਿਲੀਜ਼ ਨੋਟਸ",
"pick_color_scheme": "ਰੰਗ ਯੋਜਨਾ ਚੁਣੋ",
"save": "ਸੰਭਾਲੋ",
"choose_the_device": "ਉਪਕਰਣ ਚੁਣੋ:",
"multiple_device_connected": "ਕਈ ਉਪਕਰਣ ਜੋੜੇ ਗਏ ਹਨ।\nਉਸ ਉਪਕਰਣ ਨੂੰ ਚੁਣੋ ਜਿਸ ਤੇ ਤੁਸੀਂ ਕਿਰਿਆ ਕਰਨੀ ਚਾਹੁੰਦੇ ਹੋ",
"nothing_found": "ਕੁੱਝ ਨਹੀਂ ਮਿਲਿਆ",
"the_box_is_empty": "ਡੱਬਾ ਖਾਲੀ ਹੈ",
"top_artists": "ਚੋਟੀ ਦਾ ਕਲਾਕਾਰ",
"top_albums": "ਚੋਟੀ ਦੀ ਐਲਬਮ",
"this_week": "ਇਸ ਹਫ਼ਤੇ",
"this_month": "ਇਸ ਮਹੀਨੇ",
"last_6_months": "ਪਿਛਲੇ 6 ਮਹੀਨੇ",
"this_year": "ਇਸ ਸਾਲ",
"last_2_years": "ਪਿਛਲੇ 2 साल",
"all_time": "ਪੂਰਾ ਸਮਾਂ",
"powered_by_provider": "{providerName} ਦੁਆਰਾ ਸੰਚਾਲਿਤ",
"email": "ਈਮੇਲ",
"profile_followers": "ਫ਼ਾਲੋਅਰ",
"birthday": "ਜਨਮਦਿਨ",
"subscription": "ਗਾਹਕੀਆਂ",
"not_born": "ਹਲੇ ਪੈਦਾ ਨਹੀਂ ਹੋਇਆ",
"hacker": "ਹੈਕਰ",
"profile": "ਪ੍ਰੋਫ਼ਾਈਲ",
"no_name": "ਕੋਈ ਨਾਮ ਨਹੀਂ",
"edit": "ਸੋਧੋ",
"user_profile": "ਵਰਤੋਂਕਾਰ ਪ੍ਰੋਫ਼ਾਈਲ",
"count_plays": "{count} ਪਲੇ",
"streaming_fees_hypothetical": "ਸਟ੍ਰੀਮਿੰਗ ਫੀਸ (ਕਾਲਪਨਿਕ)",
"count_mins": "{minutes} ਮਿੰਟ",
"summary_minutes": "ਮਿੰਟ",
"summary_listened_to_music": "ਸੁਣਿਆ ਗਿਆ ਸੰਗੀਤ",
"summary_songs": "ਗੀਤ",
"summary_streamed_overall": "ਕੁੱਲ ਸਟ੍ਰੀਮ",
"summary_owed_to_artists": "ਕਲਾਕਾਰਾਂ ਨੂੰ ਦੇਣਦਾਰ\nਇਸ ਮਹੀਨੇ",
"summary_artists": "ਕਲਾਕਾਰ",
"summary_music_reached_you": "ਸੰਗੀਤ ਤੁਹਾਡੇ ਤੱਕ ਪਹੁੰਚ ਗਿਆ",
"summary_full_albums": "ਪੂਰਾ ਐਲਬਮ",
"summary_got_your_love": "ਤੁਹਾਡਾ ਪਿਆਰ ਮਿਲਿਆ",
"summary_playlists": "ਪਲੇਸੂਚੀਆਂ",
"summary_were_on_repeat": "ਦੁਹਰਾਇਆ ਗਿਆ",
"total_money": "ਕੁੱਲ {money}",
"minutes_listened": "ਮਿੰਟ ਸੁਣਿਆ",
"streamed_songs": "ਗੀਤ ਸਟ੍ਰੀਮ ਕੀਤੇ",
"count_streams": "{count} ਸਟ੍ਰੀਮ",
"owned_by_you": "ਤੁਹਾਡੀ ਮਲਕੀਅਤ",
"copied_shareurl_to_clipboard": "{shareUrl} ਕਲਿੱਪਬੋਰਡ ਉੱਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
"spotify_hipotetical_calculation": "*ਇਸਦੀ ਗਣਨਾ Spotify ਪ੍ਰਤੀ ਸਟ੍ਰੀਮ\n$0.003 ਤੋਂ $0.005 ਦੇ ਭੁਗਤਾਨ ਦੇ ਆਧਾਰ 'ਤੇ ਕੀਤੀ ਜਾਂਦੀ ਹੈ। ਇਹ ਇੱਕ ਕਾਲਪਨਿਕ\nਗਣਨਾ ਹੈ ਜੋ ਉਪਭੋਗਤਾ ਨੂੰ ਇਸ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦਿੰਦਾ ਹੈ ਕਿ \nਜੇਕਰ ਉਹਨਾਂ ਨੇ Spotify 'ਤੇ ਗੀਤ ਸੁਣਿਆ ਤਾਂ ਉਹਨਾਂ ਨੇ ਕਿੰਨਾ ਭੁਗਤਾਨ ਕੀਤਾ ਹੋਵੇਗਾ।",
"webview_not_found": "ਵੈੱਬਵਿਯੂ ਨਹੀਂ ਮਿਲਿਆ",
"webview_not_found_description": "ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਉੱਤੇ ਕੋਈ ਵੈੱਬਵਿਯੂ ਰਨਟਾਈਮ ਇੰਸਟਾਲ ਨਹੀਂ ਹੈ।\nਜੈਕਰ ਇਹ ਇੰਸਟਾਲ ਹੈ ਤਾਂ ਪੱਕਾ ਕਰੋ ਕਿ ਇਹ ਵਾਤਾਵਰਨ ਰਸਤੇ ਵਿੱਚ ਹੈ\n\nਇੰਸਟਾਲ ਕਰਨ ਤੋਂ ਬਾਅਦ, ਐਪ ਦੁਬਾਰਾ ਚਲਾਉ",
"unsupported_platform": "ਅਸਮਰਥਿਤ ਪਲੇਟਫਾਰਮ",
"invidious_instance": "ਇਨਡਿਵੀਡਿਊਸ ਸਰਵਰ ਇੰਸਟੈਂਸ",
"invidious_description": "ਟ੍ਰੈਕ ਮਿਲਾਨ ਲਈ ਇਨਡਿਵੀਡਿਊਸ ਸਰਵਰ ਇੰਸਟੈਂਸ",
"invidious_warning": "ਕੁੱਝ ਇੰਸਟੈਂਸ ਸਹੀ ਕੰਮ ਨਹੀਂ ਕਰ ਰਹੇ ਹਨ। ਆਪਣੇ ਜੋਖਮ ਤੇ ਵਰਤੋ",
"invidious_source_description": "ਪਾਈਪਡ ਦੇ ਸਮਾਨ, ਪਰ ਵੱਧ ਉਪਲਬਧਤਾ ਨਾਲ",
"cache_music": "ਸੰਗੀਤ ਨੂੰ ਕੈਸ਼ ਕਰੋ",
"open": "ਖੋਲ੍ਹੋ",
"cache_folder": "ਕੈਸ਼ ਫੋਲਡਰ",
"export": "ਨਿਰਯਾਤ ਕਰੋ",
"clear_cache": "ਕੈਸ਼ ਸਾਫ਼ ਕਰੋ",
"clear_cache_confirmation": "ਕੀ ਤੁਸੀਂ ਕੈਸ਼ ਸਾਫ਼ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"export_cache_files": "ਕੈਸ਼ ਫ਼ਾਈਲਾਂ ਨਿਰਯਾਤ ਕਰੋ",
"found_n_files": "{count} ਫ਼ਾਈਲਾਂ ਮਿਲੀਆਂ",
"export_cache_confirmation": "ਕੀ ਤੁਸੀਂ ਇਹਨਾਂ ਫ਼ਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ",
"exported_n_out_of_m_files": "{files} ਵਿੱਚੋਂ {filesExported} ਫ਼ਾਈਲਾਂ ਨਿਰਯਾਤ ਕੀਤੀਆਂ ਗਈਆਂ"
}

View File

@ -8,7 +8,7 @@ import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend;
@ -95,8 +95,9 @@ class FriendItem extends HookConsumerWidget {
text: " ${friend.track.album.name}",
recognizer: TapGestureRecognizer()
..onTap = () async {
final album =
await spotify.albums.get(friend.track.album.id);
final album = await spotify.invoke(
(api) => api.albums.get(friend.track.album.id),
);
if (context.mounted) {
context.navigateTo(
AlbumRoute(id: album.id!, album: album),

View File

@ -19,7 +19,6 @@ import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
class PlaylistCreateDialog extends HookConsumerWidget {
/// Track ids to add to the playlist
@ -260,7 +259,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
class PlaylistCreateDialogButton extends HookConsumerWidget {
const PlaylistCreateDialogButton({super.key});
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) {
showDialog(
context: context,
alignment: Alignment.center,

View File

@ -22,7 +22,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:auto_route/auto_route.dart';
@ -70,7 +69,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
leftSeedCount,
context.l10n.artists,
)),
fetchSeeds: (textEditingValue) => spotify.search
fetchSeeds: (textEditingValue) => spotify.invoke(
(api) => api.search
.get(
textEditingValue.text,
types: [SearchType.artist],
@ -86,6 +86,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
)
.toList(),
),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar(
initials: "O",
@ -146,7 +147,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
leftSeedCount,
context.l10n.tracks,
)),
fetchSeeds: (textEditingValue) => spotify.search
fetchSeeds: (textEditingValue) => spotify.invoke(
(api) => api.search
.get(
textEditingValue.text,
types: [SearchType.track],
@ -162,6 +164,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
)
.toList(),
),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar(
initials: option.name!.substring(0, 1),

View File

@ -15,6 +15,7 @@ import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';
import 'package:otp_util/otp_util.dart';
// ignore: implementation_imports
@ -197,6 +198,34 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
);
}
Future<Response> getToken({
required String totp,
required int timestamp,
String mode = "transport",
String? spDc,
}) async {
assert(mode == "transport" || mode == "init");
final accessTokenUrl = Uri.parse(
"https://open.spotify.com/get_access_token?reason=$mode&productType=web-player"
"&totp=$totp&totpVer=5&ts=$timestamp",
);
final res = await dio.getUri(
accessTokenUrl,
options: Options(
headers: {
"Cookie": spDc ?? "",
"User-Agent": ServiceUtils.randomUserAgent(
kIsDesktop ? UserAgentDevice.desktop : UserAgentDevice.mobile,
),
},
),
);
return res;
}
Future<AuthenticationTableCompanion> credentialsFromCookie(
String cookie,
) async {
@ -207,24 +236,34 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
?.trim();
final totp = await generateTotp();
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
final accessTokenUrl = Uri.parse(
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player"
"&totp=$totp&totpVer=5&ts=$timestamp",
var res = await getToken(
totp: totp,
timestamp: timestamp,
spDc: spDc,
mode: "transport",
);
final res = await dio.getUri(
accessTokenUrl,
options: Options(
headers: {
"Cookie": spDc ?? "",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
},
),
if ((res.data["accessToken"]?.length ?? 0) != 374) {
res = await getToken(
totp: totp,
timestamp: timestamp,
spDc: spDc,
mode: "init",
);
final body = res.data;
}
final body = res.data as Map<String, dynamic>;
if (body["accessToken"] == null) {
AppLogger.reportError(
"The access token is only ${body["accessToken"]?.length} characters long instead of 374\n"
"Your authentication probably doesn't work",
StackTrace.current,
);
}
return AuthenticationTableCompanion.insert(
id: const Value(0),

View File

@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart';
final customSpotifyEndpointProvider = Provider<CustomSpotifyEndpoints>((ref) {

View File

@ -76,8 +76,6 @@ final localTracksProvider =
final mime = lookupMimeType(e.path) ??
(extension(e.path) == ".opus" ? "audio/opus" : null);
print("${basename(e.path)}: $mime");
return e is File && supportedAudioTypes.contains(mime);
},
).cast<File>(),

View File

@ -22,11 +22,14 @@ class FavoriteAlbumState extends PaginatedState<AlbumSimple> {
class FavoriteAlbumNotifier
extends PaginatedAsyncNotifier<AlbumSimple, FavoriteAlbumState> {
@override
Future<List<AlbumSimple>> fetch(int offset, int limit) {
return spotify.me
.savedAlbums()
.getPage(limit, offset)
.then((value) => value.items?.toList() ?? []);
Future<List<AlbumSimple>> fetch(int offset, int limit) async {
return await spotify
.invoke(
(api) => api.me.savedAlbums().getPage(limit, offset),
)
.then(
(value) => value.items?.toList() ?? <AlbumSimple>[],
);
}
@override
@ -45,8 +48,10 @@ class FavoriteAlbumNotifier
if (state.value == null) return;
state = await AsyncValue.guard(() async {
await spotify.me.saveAlbums(ids);
final albums = await spotify.albums.list(ids);
await spotify.invoke((api) => api.me.saveAlbums(ids));
final albums = await spotify.invoke(
(api) => api.albums.list(ids),
);
return state.value!.copyWith(
items: [
@ -65,7 +70,7 @@ class FavoriteAlbumNotifier
if (state.value == null) return;
state = await AsyncValue.guard(() async {
await spotify.me.removeAlbums(ids);
await spotify.invoke((api) => api.me.removeAlbums(ids));
return state.value!.copyWith(
items: state.value!.items

View File

@ -3,8 +3,10 @@ part of '../spotify.dart';
final albumsIsSavedProvider = FutureProvider.autoDispose.family<bool, String>(
(ref, albumId) async {
final spotify = ref.watch(spotifyProvider);
return spotify.me.containsSavedAlbums([albumId]).then(
return spotify.invoke(
(api) => api.me.containsSavedAlbums([albumId]).then(
(value) => value[albumId] ?? false,
),
);
},
);

View File

@ -32,9 +32,9 @@ class AlbumReleasesNotifier
fetch(int offset, int limit) async {
final market = ref.read(userPreferencesProvider).market;
final albums = await spotify.browse
.newReleases(country: market)
.getPage(limit, offset);
final albums = await spotify.invoke(
(api) => api.browse.newReleases(country: market).getPage(limit, offset),
);
return albums.items?.map((album) => album.toAlbum()).toList() ?? [];
}

View File

@ -30,7 +30,9 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
@override
fetch(arg, offset, limit) async {
final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
final tracks = await spotify.invoke(
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
);
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
return (

View File

@ -31,9 +31,9 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
@override
fetch(arg, offset, limit) async {
final market = ref.read(userPreferencesProvider).market;
final albums = await spotify.artists
.albums(arg, country: market)
.getPage(limit, offset);
final albums = await spotify.invoke(
(api) => api.artists.albums(arg, country: market).getPage(limit, offset),
);
final items = albums.items?.toList() ?? [];

View File

@ -6,5 +6,5 @@ final artistProvider =
final spotify = ref.watch(spotifyProvider);
return spotify.artists.get(artistId);
return spotify.invoke((api) => api.artists.get(artistId));
});

View File

@ -33,9 +33,11 @@ class FollowedArtistsNotifier
@override
fetch(offset, limit) async {
final artists = await spotify.me.following(FollowingType.artist).getPage(
final artists = await spotify.invoke(
(api) => api.me.following(FollowingType.artist).getPage(
limit,
offset ?? '',
),
);
return (artists.items?.toList() ?? [], artists.after);
@ -55,7 +57,9 @@ class FollowedArtistsNotifier
Future<void> _followArtists(List<String> artistIds) async {
try {
final creds = await spotify.getCredentials();
final creds = await spotify.invoke(
(api) => api.getCredentials(),
);
await dio.post(
"https://api-partner.spotify.com/pathfinder/v1/query",
@ -93,7 +97,9 @@ class FollowedArtistsNotifier
await _followArtists(artistIds);
state = await AsyncValue.guard(() async {
final artists = await spotify.artists.list(artistIds);
final artists = await spotify.invoke(
(api) => api.artists.list(artistIds),
);
return state.value!.copyWith(
items: [
@ -110,7 +116,9 @@ class FollowedArtistsNotifier
Future<void> removeArtists(List<String> artistIds) async {
if (state.value == null) return;
await spotify.me.unfollow(FollowingType.artist, artistIds);
await spotify.invoke(
(api) => api.me.unfollow(FollowingType.artist, artistIds),
);
state = await AsyncValue.guard(() async {
final artists = state.value!.items.where((artist) {
@ -136,7 +144,9 @@ final followedArtistsProvider =
final allFollowedArtistsProvider = FutureProvider<List<Artist>>(
(ref) async {
final spotify = ref.watch(spotifyProvider);
final artists = await spotify.me.following(FollowingType.artist).all();
final artists = await spotify.invoke(
(api) => api.me.following(FollowingType.artist).all(),
);
return artists.toList();
},
);

View File

@ -3,8 +3,10 @@ part of '../spotify.dart';
final artistIsFollowingProvider = FutureProvider.family(
(ref, String artistId) async {
final spotify = ref.watch(spotifyProvider);
return spotify.me.checkFollowing(FollowingType.artist, [artistId]).then(
return spotify.invoke(
(api) => api.me.checkFollowing(FollowingType.artist, [artistId]).then(
(value) => value[artistId] ?? false,
),
);
},
);

View File

@ -5,7 +5,9 @@ final relatedArtistsProvider = FutureProvider.autoDispose
ref.cacheFor();
final spotify = ref.watch(spotifyProvider);
final artists = await spotify.artists.relatedArtists(artistId);
final artists = await spotify.invoke(
(api) => api.artists.relatedArtists(artistId),
);
return artists.toList();
});

View File

@ -7,7 +7,9 @@ final artistTopTracksProvider =
final spotify = ref.watch(spotifyProvider);
final market = ref.watch(userPreferencesProvider.select((s) => s.market));
final tracks = await spotify.artists.topTracks(artistId, market);
final tracks = await spotify.invoke(
(api) => api.artists.topTracks(artistId, market),
);
return tracks.toList();
},

View File

@ -5,14 +5,16 @@ final categoriesProvider = FutureProvider(
final spotify = ref.watch(spotifyProvider);
final market = ref.watch(userPreferencesProvider.select((s) => s.market));
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
final categories = await spotify.categories
final categories = await spotify.invoke(
(api) => api.categories
.list(
country: market,
locale: Intl.canonicalizedLocale(
locale.toString(),
),
)
.all();
.all(),
);
return categories.toList()..shuffle();
},

View File

@ -32,7 +32,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
fetch(arg, offset, limit) async {
final preferences = ref.read(userPreferencesProvider);
final playlists = await Pages<PlaylistSimple?>(
spotify,
spotify.api,
"v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}",
(json) => json == null ? null : PlaylistSimple.fromJson(json),
'playlists',

View File

@ -138,7 +138,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
SubtitleSimple? lyrics = cachedLyrics;
final token = await spotify.getCredentials();
final token = await spotify.invoke((api) => api.getCredentials());
if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) {
lyrics = await getSpotifyLyrics(token.accessToken);

View File

@ -30,9 +30,11 @@ class FavoritePlaylistsNotifier
@override
fetch(int offset, int limit) async {
final playlists = await spotify.playlists.me.getPage(
final playlists = await spotify.invoke(
(api) => api.playlists.me.getPage(
limit,
offset,
),
);
return playlists.items?.toList() ?? [];
@ -67,7 +69,9 @@ class FavoritePlaylistsNotifier
Future<void> addFavorite(PlaylistSimple playlist) async {
await update((state) async {
await spotify.playlists.followPlaylist(playlist.id!);
await spotify.invoke(
(api) => api.playlists.followPlaylist(playlist.id!),
);
return state.copyWith(
items: [...state.items, playlist],
);
@ -78,7 +82,9 @@ class FavoritePlaylistsNotifier
Future<void> removeFavorite(PlaylistSimple playlist) async {
await update((state) async {
await spotify.playlists.unfollowPlaylist(playlist.id!);
await spotify.invoke(
(api) => api.playlists.unfollowPlaylist(playlist.id!),
);
return state.copyWith(
items: state.items.where((e) => e.id != playlist.id).toList(),
);
@ -92,9 +98,11 @@ class FavoritePlaylistsNotifier
final spotify = ref.read(spotifyProvider);
await spotify.playlists.addTracks(
await spotify.invoke(
(api) => api.playlists.addTracks(
trackIds.map((id) => 'spotify:track:$id').toList(),
playlistId,
),
);
ref.invalidate(playlistTracksProvider(playlistId));
@ -105,9 +113,11 @@ class FavoritePlaylistsNotifier
final spotify = ref.read(spotifyProvider);
await spotify.playlists.removeTracks(
await spotify.invoke(
(api) => api.playlists.removeTracks(
trackIds.map((id) => 'spotify:track:$id').toList(),
playlistId,
),
);
ref.invalidate(playlistTracksProvider(playlistId));
@ -128,8 +138,8 @@ final isFavoritePlaylistProvider = FutureProvider.family<bool, String>(
return false;
}
final follows =
await spotify.playlists.followedByUsers(id, [me.value!.id!]);
final follows = await spotify
.invoke((api) => api.playlists.followedByUsers(id, [me.value!.id!]));
return follows[me.value!.id!] ?? false;
},

View File

@ -30,9 +30,8 @@ class FeaturedPlaylistsNotifier
@override
fetch(int offset, int limit) async {
final playlists = await spotify.playlists.featured.getPage(
limit,
offset,
final playlists = await spotify.invoke(
(api) => api.playlists.featured.getPage(limit, offset),
);
return playlists.items?.toList() ?? [];

View File

@ -8,7 +8,8 @@ final generatePlaylistProvider = FutureProvider.autoDispose
userPreferencesProvider.select((s) => s.market),
);
final recommendation = await spotify.recommendations
final recommendation = await spotify.invoke(
(api) => api.recommendations
.get(
limit: input.limit,
seedArtists: input.seedArtists?.toList(),
@ -26,14 +27,17 @@ final generatePlaylistProvider = FutureProvider.autoDispose
.catchError((e, stackTrace) {
AppLogger.reportError(e, stackTrace);
return Recommendations();
});
}),
);
if (recommendation.tracks?.isEmpty ?? true) {
return [];
}
final tracks = await spotify.tracks
.list(recommendation.tracks!.map((e) => e.id!).toList());
final tracks = await spotify.invoke(
(api) =>
api.tracks.list(recommendation.tracks!.map((e) => e.id!).toList()),
);
return tracks.toList();
},

View File

@ -4,7 +4,9 @@ class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
@override
FutureOr<List<Track>> build() async {
final spotify = ref.watch(spotifyProvider);
final savedTracked = await spotify.tracks.me.saved.all();
final savedTracked = await spotify.invoke(
(api) => api.tracks.me.saved.all(),
);
return savedTracked.map((e) => e.track!).toList();
}
@ -17,10 +19,14 @@ class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
final isLiked = tracks.map((e) => e.id).contains(track.id);
if (isLiked) {
await spotify.tracks.me.removeOne(track.id!);
await spotify.invoke(
(api) => api.tracks.me.removeOne(track.id!),
);
return tracks.where((e) => e.id != track.id).toList();
} else {
await spotify.tracks.me.saveOne(track.id!);
await spotify.invoke(
(api) => api.tracks.me.saveOne(track.id!),
);
return [track, ...tracks];
}
});

View File

@ -12,7 +12,9 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
@override
FutureOr<Playlist> build(String arg) {
final spotify = ref.watch(spotifyProvider);
return spotify.playlists.get(arg);
return spotify.invoke(
(api) => api.playlists.get(arg),
);
}
Future<void> create(PlaylistInput input, [ValueChanged? onError]) async {
@ -26,18 +28,22 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
state = await AsyncValue.guard(() async {
try {
final playlist = await spotify.playlists.createPlaylist(
final playlist = await spotify.invoke(
(api) => api.playlists.createPlaylist(
me.value!.id!,
input.playlistName,
collaborative: input.collaborative,
description: input.description,
public: input.public,
),
);
if (input.base64Image != null) {
await spotify.playlists.updatePlaylistImage(
await spotify.invoke(
(api) => api.playlists.updatePlaylistImage(
playlist.id!,
input.base64Image!,
),
);
}
@ -58,21 +64,27 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
await update((state) async {
try {
await spotify.playlists.updatePlaylist(
await spotify.invoke(
(api) => api.playlists.updatePlaylist(
state.id!,
input.playlistName,
collaborative: input.collaborative,
description: input.description,
public: input.public,
),
);
if (input.base64Image != null) {
await spotify.playlists.updatePlaylistImage(
await spotify.invoke(
(api) => api.playlists.updatePlaylistImage(
state.id!,
input.base64Image!,
),
);
final playlist = await spotify.playlists.get(state.id!);
final playlist = await spotify.invoke(
(api) => api.playlists.get(state.id!),
);
ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist);
return playlist;
@ -105,9 +117,11 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
final spotify = ref.read(spotifyProvider);
await spotify.playlists.addTracks(
await spotify.invoke(
(api) => api.playlists.addTracks(
trackIds.map((id) => "spotify:track:$id").toList(),
state.value!.id!,
),
);
} catch (e, stack) {
onError?.call(e);

View File

@ -30,9 +30,9 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
@override
fetch(arg, offset, limit) async {
final tracks = await spotify.playlists
.getTracksByPlaylistId(arg)
.getPage(limit, offset);
final tracks = await spotify.invoke(
(api) => api.playlists.getTracksByPlaylistId(arg).getPage(limit, offset),
);
/// Filter out tracks with null id because some personal playlists
/// may contain local tracks that are not available in the Spotify catalog

View File

@ -44,13 +44,15 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
nextOffset: 0,
);
}
final results = await spotify.search
final results = await spotify.invoke(
(api) => api.search
.get(
ref.read(searchTermStateProvider),
types: [arg],
market: ref.read(userPreferencesProvider).market,
)
.getPage(limit, offset);
.getPage(limit, offset),
);
final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();

View File

@ -5,6 +5,7 @@ import 'dart:math';
import 'package:drift/drift.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/database/database.dart';
@ -25,10 +26,10 @@ import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/wikipedia/wikipedia.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:wikipedia_api/wikipedia_api.dart';
@ -76,3 +77,57 @@ part 'utils/provider/paginated.dart';
part 'utils/provider/cursor.dart';
part 'utils/provider/paginated_family.dart';
part 'utils/provider/cursor_family.dart';
class SpotifyApiWrapper {
final SpotifyApi api;
final Ref ref;
SpotifyApiWrapper(
this.ref,
this.api,
);
bool _isRefreshing = false;
FutureOr<T> invoke<T>(
FutureOr<T> Function(SpotifyApi api) fn,
) async {
try {
return await fn(api);
} catch (e) {
if (((e is AuthorizationException && e.error == 'invalid_token') ||
e is ExpirationException) &&
!_isRefreshing) {
_isRefreshing = true;
await ref.read(authenticationProvider.notifier).refreshCredentials();
_isRefreshing = false;
return await fn(api);
}
rethrow;
}
}
}
final spotifyProvider = Provider<SpotifyApiWrapper>(
(ref) {
final authState = ref.watch(authenticationProvider);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
final wrapper = SpotifyApiWrapper(
ref,
authState.asData?.value == null
? SpotifyApi(
SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
),
)
: SpotifyApi.withAccessToken(
authState.asData!.value!.accessToken.value,
),
);
return wrapper;
},
);

View File

@ -6,5 +6,5 @@ final trackProvider =
final spotify = ref.watch(spotifyProvider);
return spotify.tracks.get(id);
return spotify.invoke((api) => api.tracks.get(id));
});

View File

@ -2,5 +2,5 @@ part of '../spotify.dart';
final meProvider = FutureProvider<User>((ref) async {
final spotify = ref.watch(spotifyProvider);
return spotify.me.get();
return spotify.invoke((api) => api.me.get());
});

View File

@ -2,7 +2,7 @@ part of '../spotify.dart';
// ignore: invalid_use_of_internal_member
mixin SpotifyMixin<T> on AsyncNotifierBase<T> {
SpotifyApi get spotify => ref.read(spotifyProvider);
SpotifyApiWrapper get spotify => ref.read(spotifyProvider);
}
extension on AutoDisposeAsyncNotifierProviderRef {

View File

@ -1,22 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) {
final authState = ref.watch(authenticationProvider);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
if (authState.asData?.value == null) {
return SpotifyApi(
SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
),
);
}
return SpotifyApi.withAccessToken(authState.asData!.value!.accessToken.value);
});

View File

@ -42,10 +42,10 @@ packages:
dependency: "direct main"
description:
name: app_links
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
version: "6.4.0"
app_links_linux:
dependency: transitive
description:

View File

@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
publish_to: "none"
version: 4.0.1+40
version: 4.0.2+41
homepage: https://spotube.krtirtho.dev
repository: https://github.com/KRTirtho/spotube
@ -13,7 +13,7 @@ environment:
flutter: ">=3.29.0"
dependencies:
app_links: ^6.3.2
app_links: ^6.4.0
args: ^2.5.0
async: ^2.11.0
audio_service: ^0.18.13