Skip to content

Commit

Permalink
Merge pull request #17 from jeroentvb/dev
Browse files Browse the repository at this point in the history
feat: auto sync combined playlists at Spotify startup
  • Loading branch information
jeroentvb authored Oct 24, 2023
2 parents c22a648 + 7548831 commit 24f491d
Show file tree
Hide file tree
Showing 12 changed files with 2,094 additions and 1,380 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Playlist combiner
Ever wanted to combine your spotify playlists into one big one? This Spicetify custom app is able to do just that.
The app currently allows combining playlist into one playlists and synchronizing them with the press of a button after adding songs to one of the source playlists. The ultimate goal is to add the songs from the source to the target automatically. But that's still w.i.p.

### Features
* Combine multiple playlists into one
* Auto sync to add missing tracks when spotify starts

![combined playlists home page](docs/home.png)

Expand Down Expand Up @@ -34,6 +37,3 @@ Playlists may also be added or removed via the edit modal. Removing a playlist d
```
⚠️ Combining a large amount of songs into a playlist may take some time
```

## Todo
See the [issues](https://github.com/jeroentvb/spicetify-combined-playlists/issues) tab.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "combined-playlists",
"version": "1.3.0",
"version": "1.4.0",
"private": true,
"scripts": {
"build": "spicetify-creator",
Expand Down
71 changes: 66 additions & 5 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import React from 'react';
import { combinePlaylists, getPlaylistInfo, getPaginatedSpotifyData, TrackState } from './utils';
import { combinePlaylists, getPlaylistInfo, getPaginatedSpotifyData, TrackState, setCombinedPlaylistsSettings, getCombinedPlaylistsSettings } from './utils';
import { CREATE_NEW_PLAYLIST_IDENTIFIER, CREATE_PLAYLIST_URL, GET_PLAYLISTS_URL, LIKED_SONGS_PLAYLIST_FACADE, LS_KEY } from './constants';
import type { CombinedPlaylist, SpotifyPlaylist, InitialPlaylistForm } from './types';
import type { CombinedPlaylist, SpotifyPlaylist, InitialPlaylistForm, CombinedPlaylistsSettings } from './types';

import './assets/css/styles.scss';
import { SpicetifySvgIcon } from './components/SpicetifySvgIcon';
import { PlaylistForm } from './components/AddPlaylistForm';
import { AddPlaylistCard } from './components/AddPlaylistCard';
import { Card } from './components/Card';
import { ImportExportModal } from './components/ImportExportModal';
import { synchronizeCombinedPlaylists } from './extensions/auto-sync';

export interface State {
playlists: SpotifyPlaylist[];
combinedPlaylists: CombinedPlaylist[];
isLoading: boolean;
isInitializing: boolean;
autoSync: boolean;
}

// Needs to be deinfed to avoid eslint error
const SpotifyComponents = Spicetify.ReactComponent;

class App extends React.Component<Record<string, unknown>, State> {

get combinedPlaylistsLs(): CombinedPlaylist[] {
Expand All @@ -30,11 +35,14 @@ class App extends React.Component<Record<string, unknown>, State> {
constructor(props: Record<string, unknown>) {
super(props);

const settings = getCombinedPlaylistsSettings();

this.state = {
playlists: [],
combinedPlaylists: [],
isLoading: false,
isInitializing: false,
autoSync: settings.autoSync,
};
}

Expand Down Expand Up @@ -67,7 +75,11 @@ class App extends React.Component<Record<string, unknown>, State> {
? await this.createPlaylist(formData.sources)
: this.findPlaylist(formData.target);

await combinePlaylists(sourcePlaylists, targetPlaylist);
await combinePlaylists(sourcePlaylists, targetPlaylist)
.catch((err) => {
console.error('An error ocurred while combining playlists', err);
Spicetify.showNotification('An error ocurred while combining playlists', true);
});
this.saveCombinedPlaylist(sourcePlaylists, targetPlaylist);

Spicetify.PopupModal.hide();
Expand Down Expand Up @@ -120,7 +132,17 @@ class App extends React.Component<Record<string, unknown>, State> {
const { sources } = this.state.combinedPlaylists.find((combinedPlaylist) => combinedPlaylist.target.id === playlistToSync.id) as CombinedPlaylist;
const sourcePlaylists = sources.map((sourcePlaylist) => this.findPlaylist(sourcePlaylist.id));

await combinePlaylists(sourcePlaylists, playlistToSync);
await combinePlaylists(sourcePlaylists, playlistToSync)
.catch((err) => {
console.error('An error ocurred while syncing playlists', err);
Spicetify.showNotification('An error ocurred while syncing playlists', true);
});
}

@TrackState('isLoading')
async syncAllPlaylists() {
Spicetify.showNotification('Synchronizing all combined playlists');
await synchronizeCombinedPlaylists();
}

findPlaylist(id: string): SpotifyPlaylist {
Expand Down Expand Up @@ -182,13 +204,52 @@ class App extends React.Component<Record<string, unknown>, State> {
});
}

toggleAutoSuync() {
const newSettings: CombinedPlaylistsSettings = {
...getCombinedPlaylistsSettings(),
autoSync: !this.state.autoSync,
};

this.setState({ autoSync: newSettings.autoSync });
setCombinedPlaylistsSettings(newSettings);
}

render() {
const menuWrapper = (<SpotifyComponents.Menu>
<SpotifyComponents.MenuItem
label="Import / export combined playlists"
leadingIcon={<SpicetifySvgIcon iconName="external-link" />}
onClick={() => this.openImportExportModal()}
>
Import / export
</SpotifyComponents.MenuItem>
<SpotifyComponents.MenuItem
label="Toggle auto sync"
leadingIcon={<SpicetifySvgIcon iconName="repeat" />}
onClick={() => this.toggleAutoSuync()}
>
{this.state.autoSync ? 'Disable auto sync' : 'Enable auto sync'}
</SpotifyComponents.MenuItem>
<SpotifyComponents.MenuItem
label="Synchronize all combined playlists"
leadingIcon={<SpicetifySvgIcon iconName="repeat-once" />}
onClick={() => !this.state.isLoading && this.syncAllPlaylists()}
>
Synchronize all
</SpotifyComponents.MenuItem>
</SpotifyComponents.Menu>);

return (
<div id="combined-playlists--wrapper" className="contentSpacing">
<header>
<h1>Playlist combiner</h1>
<button onClick={() => this.openImportExportModal()}><SpicetifySvgIcon iconName="external-link" /></button>
<button onClick={() => this.showAddPlaylistModal()}><SpicetifySvgIcon iconName="plus2px" /></button>
<SpotifyComponents.ContextMenu
trigger="click"
menu={menuWrapper}
>
<button><SpicetifySvgIcon iconName="more" /></button>
</SpotifyComponents.ContextMenu>
</header>

{!this.state.isInitializing && <div id="combined-playlists--grid" className="main-gridContainer-gridContainer">
Expand Down
8 changes: 6 additions & 2 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ export const GET_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists?limit=
*/
export const TRACKS_FROM_PLAYLIST_URL_FILTER = '?fields=items(track(uri)),next';

export const GET_PLAYLIST_TRACKS_URL = (uri: string) => `sp://core-playlist/v1/playlist/${uri}/rows`;

export const GET_LIKED_SONGS_LIST_URL = 'sp://core-collection/unstable/@/list/tracks/all?responseFormat=protobufJson';

export const ADD_TRACKS_TO_PLAYLIST_URL = (id: string) => `https://api.spotify.com/v1/playlists/${id}/tracks`;

export const LS_KEY = 'combined-playlists';
export const LS_KEY_SETTINGS = 'combined-playlists-settings';

export const CREATE_PLAYLIST_URL = (userId: string) => `https://api.spotify.com/v1/users/${userId}/playlists`;

export const CREATE_NEW_PLAYLIST_IDENTIFIER = 'CREATE_NEW_PLAYLIST_IDENTIFIER';

export const LIKED_SONGS_PLAYLIST_FACADE: SpotifyPlaylist = {
name: Spicetify.Platform.Translations['shared.library.entity-row.liked-songs.title'],
name: Spicetify.Platform?.Translations['shared.library.entity-row.liked-songs.title'],
collaborative: false,
description: '',
external_urls: { spotify: '' },
Expand All @@ -39,5 +44,4 @@ export const LIKED_SONGS_PLAYLIST_FACADE: SpotifyPlaylist = {
},
type: 'playlist',
uri: 'spotify:playlist:liked-songs-facade'

};
22 changes: 22 additions & 0 deletions src/extensions/auto-sync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LS_KEY } from '../constants';
import type { CombinedPlaylist } from '../types';
import { combinePlaylists, getCombinedPlaylistsSettings } from '../utils';

(async () => {
while (!Spicetify?.Platform || !Spicetify?.CosmosAsync) {
await new Promise(resolve => setTimeout(resolve, 100));
}

const autoSync = getCombinedPlaylistsSettings().autoSync;
if (autoSync) synchronizeCombinedPlaylists();
})();

export function synchronizeCombinedPlaylists() {
const combinedPlaylists: CombinedPlaylist[] = JSON.parse(Spicetify.LocalStorage.get(LS_KEY) as string) ?? [];

return Promise.all(combinedPlaylists.map(({ sources, target }) => combinePlaylists(sources, target, true)))
.catch((err) => {
console.error('An error ocurred while auto-syncing playlists', err);
Spicetify.showNotification('An error ocurred while auto-syncing playlists', true);
});
}
3 changes: 3 additions & 0 deletions src/types/combined-playlists-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface CombinedPlaylistsSettings {
autoSync: boolean;
}
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './combined-playlist';
export * from './initial-playlist-form';
export * from './spotify-playlist';
export * from './collection-call-response';
export * from './playlist-tracks-response';
export * from './combined-playlists-settings';
5 changes: 5 additions & 0 deletions src/types/playlist-tracks-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PlaylistRowsResponse {
rows: {
link: string
}[]
}
Loading

1 comment on commit 24f491d

@qdesjo
Copy link

@qdesjo qdesjo commented on 24f491d Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this incredible new feature.!

Please sign in to comment.