Skip to content

Commit

Permalink
Merge pull request #9 from C0ntrolDev/dev-1.1
Browse files Browse the repository at this point in the history
Ver 1.1.0
  • Loading branch information
C0ntrolDev authored Aug 6, 2024
2 parents 5965fec + 924843d commit d50c90b
Show file tree
Hide file tree
Showing 22 changed files with 131 additions and 99 deletions.
58 changes: 44 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
# spotify_downloader

Spotify Downloader is an application that allows you to download your favorite playlists at the touch of just one button!
<div align="center">
<img src="https://raw.githubusercontent.com/C0ntrolDev/spotify_downloader/main/github_images/app_icon.png" width="100"/>
</div>
<h1 align="center">
Spotify Downloader
</h1>
<div align="center">
App that allows you to download your favorite playlists at the touch of just one button!
</div>

## Screenshots

<img src="https://github.com/C0ntrolDev/spotify_downloader/blob/main/github_images/main_screen.jpg" width="200" /> <img src="https://github.com/C0ntrolDev/spotify_downloader/blob/main/github_images/playlist_screen.jpg" width="200" /> <img src="https://github.com/C0ntrolDev/spotify_downloader/blob/main/github_images/change_source_screen.jpg" width="200" />
<img src="https://raw.githubusercontent.com/C0ntrolDev/spotify_downloader/main/github_images/main_screen.png" width="200" /> <img src="https://raw.githubusercontent.com/C0ntrolDev/spotify_downloader/main/github_images/download_screen.png" width="200" /> <img src="https://raw.githubusercontent.com/C0ntrolDev/spotify_downloader/main/github_images/change_source_screen.png" width="200" />

## About App

- 📱 🍎 Supported platforms: Android, IOS
- 🇺🇸 🇷🇺 Languages: English, Russian
- 🎥 Download audio from YouTube using SpotifyAPI

## Features

- 📥 Download the entire playlist with one click
- ❤️ Download favorite tracks
- 🔄 Change the download source if you didn't like the automatic choice
- 🔔 Track downloads in notifications or on the main page
- 💤 Background download (Android only)

## How to download

### Android

You can download this apk if you don't know what kind of architecture you have.
- [spotify_downloader.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.0.3/spotify_downloader.apk)
- [spotify_downloader.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.1.0/spotify_downloader.apk)

If you know what architecture you have, then download one of the apk listed below.
- [spotify_downloader_armeabi-v7a.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.0.3/spotify_downloader_armeabi-v7a.apk)
- [spotify_downloader_arm64-v8a.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.0.3/spotify_downloader_arm64-v8a.apk)
- [spotify_downloader_x86_64.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.0.3/spotify_downloader_x86_64.apk)
- [spotify_downloader_armeabi-v7a.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.1.0/spotify_downloader_armeabi-v7a.apk)
- [spotify_downloader_arm64-v8a.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.1.0/spotify_downloader_arm64-v8a.apk)
- [spotify_downloader_x86_64.apk](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.1.0/spotify_downloader_x86_64.apk)

### IOS

You can download ipa there.
- [spotify_downloader.ipa](https://github.com/C0ntrolDev/spotify_downloader/releases/download/v1.1.0/spotify_downloader.ipa)

## How to use

After you have downloaded the application, you must grant it the permissions it will ask for.
Then you can use the app. Just paste the link to the playlist and click the search button.

But in this case, your favorite tracks will not be available for download, as well as playlists that are created "only for you" may not match those that you expect
If you want to download your favorite tracks, and also download playlists "only for you", you must create Spotify Service App

## How to download liked tracks
## How create Spotify Service App and use it

To do this, you will need to create your own spotify app.
1. Follow this link and login - https://developer.spotify.com/
2. Follow this link and create your own app - https://developer.spotify.com/dashboard/create
- __App name__ - whatever you want
Expand All @@ -39,10 +64,15 @@ To do this, you will need to create your own spotify app.

__After logging in, you can download your favorite tracks, as well as playlists "only for you"__

## About app
- Supported platforms: Android
- Background download: supported


## Additional information
- ⭐ I would appreciate it if you **star this repository!**

## For developers
1. Developed on Flutter
2. When developing the application, I used a "Clean Architecture". I don't think this application is an ideal representative of this approach. But if you want, you can use it as an example project.

<h3 align="center">
^_^
</h3>
Binary file added github_images/app_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed github_images/change_source_screen.jpg
Binary file not shown.
Binary file added github_images/change_source_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added github_images/download_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed github_images/main_screen.jpg
Binary file not shown.
Binary file added github_images/main_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed github_images/playlist_screen.jpg
Binary file not shown.
4 changes: 2 additions & 2 deletions lib/core/consts/spotify_client.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const deffaultClientId = 'e6187f63ef044f3cb72b5cb6707b66a9';
const deffaultClientSecret = '2dd4ff57587e4029938bcef3ec258b34';
const defaultClientId = 'e6187f63ef044f3cb72b5cb6707b66a9';
const defaultClientSecret = '2dd4ff57587e4029938bcef3ec258b34';
const clientScopes = ['playlist-read-private', 'user-library-read', 'user-read-email', 'user-read-private'];
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ class LocalAuthRepositoryImpl implements LocalFullAuthRepository, LocalClientAut
return Result.isSuccessful(authCredentials);
} else {
return Result.isSuccessful(FullCredentials(
clientId: deffaultClientId,
clientSecret: deffaultClientSecret,
clientId: defaultClientId,
clientSecret: defaultClientSecret,
refreshToken: null,
accessToken: null,
expiration: null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ class SettingsDataSource {
Future<Result<Failure, AppSettings>> getDefaultAppSettings() async {
try {
String? defaultSavePath;
if (defaultTargetPlatform != TargetPlatform.iOS) {
final downloadsDirectoryPath = (await getDownloadsDirectory())?.path;
if (downloadsDirectoryPath != null) {
if (defaultTargetPlatform == TargetPlatform.android) {
const downloadsDirectoryPath = "/storage/emulated/0/Download";
if (await Directory.fromUri(Uri.parse(downloadsDirectoryPath)).exists()) {
defaultSavePath = "$downloadsDirectoryPath/SpotifyDownloader";
}
}

defaultSavePath ??= "${(await getApplicationDocumentsDirectory()).path}/SpotifyDownloader";

const defaultSaveMode = 0;
Expand All @@ -68,7 +68,7 @@ class SettingsDataSource {
}

List<String> getAvailableLanguages() {
return S.delegate.supportedLocales.where((l) => l.countryCode != null).map((l) => l.countryCode!).toList();
return S.delegate.supportedLocales.map((l) => l.languageCode).toList();
}

String _appSettingsToJson(AppSettings appSettings) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:spotify_downloader/core/utils/utils.dart';
import 'package:spotify_downloader/features/data_domain/tracks/services/services.dart';
Expand Down Expand Up @@ -27,7 +26,7 @@ class DownloadTracksCubit extends Cubit<DownloadTracksState> {
_downloadTracksFromGettingObserver = downloadTracksFromGettingObserver,
_downloadTrack = downloadTrack,
_cancelTrackLoading = cancelTrackLoading,
super(const DownloadTracksDeffault(preselectedTracksYouTubeUrls: {}));
super(const DownloadTracksDefault(preselectedTracksYouTubeUrls: {}));

Future<void> downloadAllTracks() async {
if (_trackList != null && _trackList!.isNotEmpty) {
Expand Down Expand Up @@ -102,7 +101,7 @@ class DownloadTracksCubit extends Cubit<DownloadTracksState> {
_preselectedTracksYouTubeUrls[trackWithLoadingObserver] = newYoutubeUrl;

cancelTrackLoading(trackWithLoadingObserver);
emit(DownloadTracksDeffault(preselectedTracksYouTubeUrls: _preselectedTracksYouTubeUrls));
emit(DownloadTracksDefault(preselectedTracksYouTubeUrls: _preselectedTracksYouTubeUrls));
}

void setGettingObserver(TracksWithLoadingObserverGettingObserver? gettingObserver) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
part of 'download_tracks_cubit.dart';

sealed class DownloadTracksState extends Equatable {
sealed class DownloadTracksState {
final Map<TrackWithLoadingObserver, String> preselectedTracksYouTubeUrls;

const DownloadTracksState({required this.preselectedTracksYouTubeUrls});

@override
List<Object?> get props => [];
}

final class DownloadTracksDeffault extends DownloadTracksState {
const DownloadTracksDeffault({required super.preselectedTracksYouTubeUrls});

@override
List<Object?> get props => [];
final class DownloadTracksDefault extends DownloadTracksState {
const DownloadTracksDefault({required super.preselectedTracksYouTubeUrls});
}

final class DownloadTracksFailure extends DownloadTracksState {
const DownloadTracksFailure({required this.failure, required super.preselectedTracksYouTubeUrls});

final Failure? failure;

@override
List<Object?> get props => [failure];
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import 'package:spotify_downloader/features/data_domain/tracks/services/entities
part 'filter_tracks_event.dart';
part 'filter_tracks_state.dart';

class FilterTracksBloc extends Bloc<FilterTracksEvent, FilterTracksDeffault> {
class FilterTracksBloc extends Bloc<FilterTracksEvent, FilterTracksDefault> {
String? _filterQuery;
List<TrackWithLoadingObserver>? _tracksSource;

FilterTracksBloc() : super(FilterTracksDeffault(filteredTracks: List.empty(), isFilterQueryEmpty: true)) {
FilterTracksBloc() : super(FilterTracksDefault(filteredTracks: List.empty(), isFilterQueryEmpty: true)) {
on<FilterTracksChangeSource>((event, emit) {
_tracksSource = event.newSource;
emit(FilterTracksDeffault(filteredTracks: getFilteredTracks(), isFilterQueryEmpty: _filterQuery?.isEmpty ?? true));
emit(FilterTracksDefault(filteredTracks: getFilteredTracks(), isFilterQueryEmpty: _filterQuery?.isEmpty ?? true));
});

on<FilterTracksChangeFilterQuery>((event, emit) {
_filterQuery = event.newQuery;
emit(FilterTracksDeffault(filteredTracks: getFilteredTracks(), isFilterQueryEmpty: _filterQuery?.isEmpty ?? true));
emit(FilterTracksDefault(filteredTracks: getFilteredTracks(), isFilterQueryEmpty: _filterQuery?.isEmpty ?? true));
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
part of 'filter_tracks_bloc.dart';

final class FilterTracksDeffault extends Equatable {
const FilterTracksDeffault({required this.filteredTracks, required this.isFilterQueryEmpty});
final class FilterTracksDefault extends Equatable {
const FilterTracksDefault({required this.filteredTracks, required this.isFilterQueryEmpty});

final List<TrackWithLoadingObserver> filteredTracks;
final bool isFilterQueryEmpty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ class _DownloadTracksCollectionScreenState extends State<DownloadTracksCollectio
final getTracksCollectionState = context.watch<GetTracksCollectionBloc>().state;
final getTracksState = context.watch<GetTracksBloc>().state;
final filteredTracksState = context.watch<FilterTracksBloc>().state;
final donwloadTracksState = context.watch<DownloadTracksCubit>().state;

context.watch<DownloadTracksCubit>().state;

if (getTracksCollectionState is GetTracksCollectionNetworkFailure) {
return NetworkFailureSplash(
Expand Down Expand Up @@ -276,31 +277,18 @@ class _DownloadTracksCollectionScreenState extends State<DownloadTracksCollectio
}

final trackWithLoadingObserver = filteredTracks[index];
final preselectedYoutubeUrl = donwloadTracksState
.preselectedTracksYouTubeUrls[trackWithLoadingObserver];

final currentYoutubeUrl = preselectedYoutubeUrl ??
trackWithLoadingObserver.loadingObserver?.youtubeUrl ??
trackWithLoadingObserver.track.localYoutubeUrl;

return TracksCollectionTypeDependTrackTile(
type: getTracksCollectionState.tracksCollection.type,
isLoadedIfLoadingObserverIsNull: preselectedYoutubeUrl != null
? false
: trackWithLoadingObserver.track.isLoaded,
trackWithLoadingObserver: trackWithLoadingObserver,
isLoadedIfLoadingObserverIsNull:
_getIsTrackLoadedIfLoadingObserverIsNull(trackWithLoadingObserver),
onDownloadButtonClicked: () =>
_downloadTracksCubit.downloadTrack(trackWithLoadingObserver),
onCancelButtonClicked: () =>
_downloadTracksCubit.cancelTrackLoading(trackWithLoadingObserver),
onMoreInfoClicked: () => showDownloadTrackInfoBottomSheet(
initialYoutubeUrl: currentYoutubeUrl,
context: context,
trackWithLoadingObserver: trackWithLoadingObserver,
onChangeYoutubeUrlClicked: () {
return _onChangeYoutubeUrlClicked(
filteredTracks[index], currentYoutubeUrl);
}),
onMoreInfoClicked: () =>
_onMoreInfoButtonClicked(context, trackWithLoadingObserver),
);
}),
),
Expand Down Expand Up @@ -354,6 +342,29 @@ class _DownloadTracksCollectionScreenState extends State<DownloadTracksCollectio
);
}

void _onMoreInfoButtonClicked(BuildContext context, TrackWithLoadingObserver trackWithLoadingObserver) {
return showDownloadTrackInfoBottomSheet(
getCurrentYoutubeUrl: () => _getCurrentUrlOfTrack(trackWithLoadingObserver),
getIsTrackLoadedIfLoadingObserverIsNull: () =>
_getIsTrackLoadedIfLoadingObserverIsNull(trackWithLoadingObserver),
context: context,
trackWithLoadingObserver: trackWithLoadingObserver,
onChangeYoutubeUrlClicked: () =>
_onChangeYoutubeUrlClicked(trackWithLoadingObserver, _getCurrentUrlOfTrack(trackWithLoadingObserver)));
}

String? _getCurrentUrlOfTrack(TrackWithLoadingObserver trackWithLoadingObserver) {
final preselectedYoutubeUrl = _downloadTracksCubit.state.preselectedTracksYouTubeUrls[trackWithLoadingObserver];
return preselectedYoutubeUrl ??
trackWithLoadingObserver.loadingObserver?.youtubeUrl ??
trackWithLoadingObserver.track.localYoutubeUrl;
}

bool _getIsTrackLoadedIfLoadingObserverIsNull(TrackWithLoadingObserver trackWithLoadingObserver) {
final preselectedYoutubeUrl = _downloadTracksCubit.state.preselectedTracksYouTubeUrls[trackWithLoadingObserver];
return preselectedYoutubeUrl != null ? false : trackWithLoadingObserver.track.isLoaded;
}

void _scheduleHeaderHeightUpdate() {
SchedulerBinding.instance.addPostFrameCallback((_) {
var newHeaderHeight = (_headerKey.currentContext!.findRenderObject() as RenderSliver).geometry?.scrollExtent;
Expand All @@ -369,23 +380,24 @@ class _DownloadTracksCollectionScreenState extends State<DownloadTracksCollectio
_filterTracksBloc.add(FilterTracksChangeFilterQuery(newQuery: newQuery));

void _onDownloadAllButtonClicked(
{required FilterTracksDeffault filterTracksState, required GetTracksState getTracksState}) {
{required FilterTracksDefault filterTracksState, required GetTracksState getTracksState}) {
if (!filterTracksState.isFilterQueryEmpty) {
_downloadTracksCubit.downloadTracksRange(filterTracksState.filteredTracks);
} else {
_downloadTracksCubit.downloadAllTracks();
}
}

Future<String?> _onChangeYoutubeUrlClicked(
Future<bool> _onChangeYoutubeUrlClicked(
TrackWithLoadingObserver trackWithLoadingObserver, String? oldYoutubeUrl) async {
final changedUrl = await AutoRouter.of(context)
.push<String?>(ChangeSourceVideoRoute(track: trackWithLoadingObserver.track, oldYoutubeUrl: oldYoutubeUrl));
if (changedUrl != null) {
_downloadTracksCubit.changeTrackYoutubeUrl(trackWithLoadingObserver, changedUrl);
return true;
}

return changedUrl;
return false;
}

Future<void> _generateBackgroundGradientColor(String? imageUrl) async {
Expand Down
Loading

0 comments on commit d50c90b

Please sign in to comment.