Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add playlist folders support #518

Merged
merged 11 commits into from
Aug 18, 2024
Merged

add playlist folders support #518

merged 11 commits into from
Aug 18, 2024

Conversation

aNNiMON
Copy link
Contributor

@aNNiMON aNNiMON commented Jul 28, 2024

Resolves #453

Note

Spotify doesn't have an API to retrieve or modify playlist folders. In order to retrieve a folder structure a third party utility is required mikez/spotify-folders. Using this utility you need to create a compatible json file and put it in ~/.cache/spotify-player/PlaylistFolders_cache.json. You'll need to update this file every time you change your folder structure.

Demo

20240728_150039.mp4

Configuration

Follow the mikez/spotify-folders installation instructions. Then run

spotifyfolders > ~/.cache/spotify-player/PlaylistFolders_cache.json

It generates a full playlist folders structure and stores to a json file, like this:

{
  "type": "folder",
  "children": [
    {
      "type": "playlist",
      "uri": "spotify:playlist:00000000000000000000"
    },
    {
      "name": "Folder name",
      "type": "folder",
      "uri": "spotify:user:aaaaaaaaaaa:folder:bbbbbbbbb",
      "children": [
        {
          "type": "playlist",
          "uri": "spotify:playlist:111111111111111111111"
        },
        {
          "type": "playlist",
          "uri": "spotify:playlist:222222222222222222222"
        }
      ]
    }
  ]
}

Copy link
Owner

@aome510 aome510 left a comment

Choose a reason for hiding this comment

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

I did a quick 1st pass. Looking to the linked video, pretty impressive work!

spotify_player/src/client/mod.rs Outdated Show resolved Hide resolved
spotify_player/src/state/data.rs Outdated Show resolved Hide resolved
spotify_player/src/state/model.rs Outdated Show resolved Hide resolved
spotify_player/src/state/ui/page.rs Outdated Show resolved Hide resolved
spotify_player/src/state/ui/popup.rs Outdated Show resolved Hide resolved
spotify_player/src/state/model.rs Outdated Show resolved Hide resolved
@aNNiMON
Copy link
Contributor Author

aNNiMON commented Aug 1, 2024

How it works

  1. spotifyfolders generates a json file with folder structure:
{
  "type": "folder", // root node is always a folder
  "children": [
    {
      "type": "playlist", // playlist type (contains uri)
      "uri": "spotify:playlist:00000000000000000000"
    },
    {
      "name": "Folder",
      "type": "folder", // folder type (contains name, uri and 0..N children)
      "uri": "spotify:user:aaaaaaaaaaa:folder:bbbbbbbbb",
      "children": [ /*...*/ ]
    }
  ]
}
  1. Imagine we have a playlist folder structure like this:
playlist: Playlist 1
folder: Folder 1
    playlist: Playlist 1.1
    folder: Folder 1A
        playlist: Playlist 1A1
    playlist: Playlist 1.2
    folder: Folder 1B
        playlist: Playlist 1B1
        playlist: Playlist 1B2
folder: Folder 2
    playlist: Playlist 2.1
    playlist: Playlist 2.2
  1. To represent it in a flat view like we have now, we need to introduce the following fields:

    • is_folder: bool — false for playlist, true for folder
    • current_level: usize — what level the playlist or the folder belongs to
    • target_level: usize — for folders: what level the folder points to. Unique for each folder.

    3.1. Why must the target level be unique? To avoid ambiguity when navigating to a folder:

    playlist: Playlist 1 // (0, 0)
    folder: Folder 1 // (0, 1)
        playlist: Playlist 1.1 // (1, 1)
    folder: Folder 2 // (0, 1)
        playlist: Playlist 2.1 // (1, 1)
        playlist: Playlist 2.2 // (1, 1)

    If we want to render playlists in Folder 1 on a level 1, we'll render:

    playlist: Playlist 1.1 // (1, 1)
    playlist: Playlist 2.1 // (1, 1)
    playlist: Playlist 2.2 // (1, 1)

    The last two playlists aren't what we want to see.

    3.2 Correct representation using unique target levels:

    playlist: Playlist 1 // (0, 0)
    folder: Folder 1 // (0, 1)
        playlist: Playlist 1.1 // (1, 1)
        folder: Folder 1A // (1, 2)
            playlist: Playlist 1A1 // (2, 2)
        playlist: Playlist 1.2 // (1, 1)
        folder: Folder 1B // (1, 3)
            playlist: Playlist 1B1 // (3, 3)
            playlist: Playlist 1B2 // (3, 3)
    folder: Folder 2 // (0, 4)
        playlist: Playlist 2.1 // (4, 4)
        playlist: Playlist 2.2 // (4, 4)

    3.3 To jump back to a previous folder, an additional playlist folder with the swapped target and source levels is needed:

    playlist: Playlist 1 // (0, 0)
    folder: Folder 1 // (0, 1)
        folder:  Folder 1 // (1, 0) here
        playlist: Playlist 1.1 // (1, 1)
        folder: Folder 1A // (1, 2)
            folder:  Folder 1A // (2, 1) here
            playlist: Playlist 1A1 // (2, 2)
        playlist: Playlist 1.2 // (1, 1)
        folder: Folder 1B // (1, 3)
            folder:  Folder 1B // (3, 1) here
            playlist: Playlist 1B1 // (3, 3)
            playlist: Playlist 1B2 // (3, 3)
    folder: Folder 2 // (0, 4)
        folder:  Folder 2 // (4, 0) here
        playlist: Playlist 2.1 // (4, 4)
        playlist: Playlist 2.2 // (4, 4)
  2. By reading the PlaylistFolders_cache.json and passing it together with the flat Playlists retrieved from the API to the structurize function, we can get a playlist folder representation as a regular flat vector.

  3. If the PlaylistFolders_cache doesn't contain some playlists, they'll go to a root folder level: (0, 0).

    // 2. Add root playlists that don't belong to folders
    let mut playlist_folders: Vec<Playlist> = Vec::new();
    for playlist in &playlists {
    if !playlist_ids.contains(playlist.id.id()) {
    let mut p = playlist.clone();
    p.is_folder = false;
    p.level = (0, 0);
    playlist_folders.push(p);
    }
    }

    Same for newly created playlists.

  4. A playlist folder uri is not supported by the application at this time, but it's needed for proper playlist folder structuring. Moreover, I prepend the id with f for folders and u for up node, so folder's uri shouldn't be used in the API calls:

id: PlaylistId::from_id("f".to_string() + id)
.unwrap()
.into_static(),
collaborative: false,
name: f.name.clone().unwrap_or_default(),

acc.push(Playlist {
id: PlaylistId::from_id("u".to_string() + id)
.unwrap()
.into_static(),
collaborative: false,
name: "← ".to_string() + f.name.clone().unwrap_or_default().as_str(),

@aome510
Copy link
Owner

aome510 commented Aug 4, 2024

How it works

  1. spotifyfolders generates a json file with folder structure:
{
  "type": "folder", // root node is always a folder
  "children": [
    {
      "type": "playlist", // playlist type (contains uri)
      "uri": "spotify:playlist:00000000000000000000"
    },
    {
      "name": "Folder",
      "type": "folder", // folder type (contains name, uri and 0..N children)
      "uri": "spotify:user:aaaaaaaaaaa:folder:bbbbbbbbb",
      "children": [ /*...*/ ]
    }
  ]
}
  1. Imagine we have a playlist folder structure like this:
playlist: Playlist 1
folder: Folder 1
    playlist: Playlist 1.1
    folder: Folder 1A
        playlist: Playlist 1A1
    playlist: Playlist 1.2
    folder: Folder 1B
        playlist: Playlist 1B1
        playlist: Playlist 1B2
folder: Folder 2
    playlist: Playlist 2.1
    playlist: Playlist 2.2
  1. To represent it in a flat view like we have now, we need to introduce the following fields:
  • is_folder: bool — false for playlist, true for folder
  • current_level: usize — what level the playlist or the folder belongs to
  • target_level: usize — for folders: what level the folder points to. Unique for each folder.

3.1. Why must the target level be unique? To avoid ambiguity when navigating to a folder:

playlist: Playlist 1 // (0, 0)
folder: Folder 1 // (0, 1)
    playlist: Playlist 1.1 // (1, 1)
folder: Folder 2 // (0, 1)
    playlist: Playlist 2.1 // (1, 1)
    playlist: Playlist 2.2 // (1, 1)

If we want to render playlists in Folder 1 on a level 1, we'll render:

playlist: Playlist 1.1 // (1, 1)
playlist: Playlist 2.1 // (1, 1)
playlist: Playlist 2.2 // (1, 1)

The last two playlists aren't what we want to see.
3.2 Correct representation using unique target levels:

playlist: Playlist 1 // (0, 0)
folder: Folder 1 // (0, 1)
    playlist: Playlist 1.1 // (1, 1)
    folder: Folder 1A // (1, 2)
        playlist: Playlist 1A1 // (2, 2)
    playlist: Playlist 1.2 // (1, 1)
    folder: Folder 1B // (1, 3)
        playlist: Playlist 1B1 // (3, 3)
        playlist: Playlist 1B2 // (3, 3)
folder: Folder 2 // (0, 4)
    playlist: Playlist 2.1 // (4, 4)
    playlist: Playlist 2.2 // (4, 4)

3.3 To jump back to a previous folder, an additional playlist folder with the swapped target and source levels is needed:

playlist: Playlist 1 // (0, 0)
folder: Folder 1 // (0, 1)
    folder:  Folder 1 // (1, 0) here
    playlist: Playlist 1.1 // (1, 1)
    folder: Folder 1A // (1, 2)
        folder:  Folder 1A // (2, 1) here
        playlist: Playlist 1A1 // (2, 2)
    playlist: Playlist 1.2 // (1, 1)
    folder: Folder 1B // (1, 3)
        folder:  Folder 1B // (3, 1) here
        playlist: Playlist 1B1 // (3, 3)
        playlist: Playlist 1B2 // (3, 3)
folder: Folder 2 // (0, 4)
    folder:  Folder 2 // (4, 0) here
    playlist: Playlist 2.1 // (4, 4)
    playlist: Playlist 2.2 // (4, 4)
  1. By reading the PlaylistFolders_cache.json and passing it together with the flat Playlists retrieved from the API to the structurize function, we can get a playlist folder representation as a regular flat vector.

  2. If the PlaylistFolders_cache doesn't contain some playlists, they'll go to a root folder level: (0, 0).

    // 2. Add root playlists that don't belong to folders
    let mut playlist_folders: Vec<Playlist> = Vec::new();
    for playlist in &playlists {
    if !playlist_ids.contains(playlist.id.id()) {
    let mut p = playlist.clone();
    p.is_folder = false;
    p.level = (0, 0);
    playlist_folders.push(p);
    }
    }

    Same for newly created playlists.

  3. A playlist folder uri is not supported by the application at this time, but it's needed for proper playlist folder structuring. Moreover, I prepend the id with f for folders and u for up node, so folder's uri shouldn't be used in the API calls:

id: PlaylistId::from_id("f".to_string() + id)
.unwrap()
.into_static(),
collaborative: false,
name: f.name.clone().unwrap_or_default(),

acc.push(Playlist {
id: PlaylistId::from_id("u".to_string() + id)
.unwrap()
.into_static(),
collaborative: false,
name: "← ".to_string() + f.name.clone().unwrap_or_default().as_str(),

Thanks for the detailed explanation. I get the idea now.

Using level is confusing and level is not meant to be unique. (current_level, target_level) should be replaced (current_id, target_id) and should include a comment that a folder points to another folder represented by target_id.

In addition, I don't think it's a good idea to modify playlist to support representing folder because playlist and folder are separate identities. My suggestion is to define a separate struct for folder and another enum PlaylistFolderItem to represent either a folder or a playlist for use in action/UI state. IMHO, separating the data representation of playlist and folder also improves the code clarity and simplifies the implementation.

@aNNiMON
Copy link
Contributor Author

aNNiMON commented Aug 4, 2024

@aome510 for me current_id is misleading because we also have a Spotify id "spotify:user:xxx:folder:yyy". What about current_folder_id / target_folder_id and comment that it's a local id, not just a global Spotify id? Or current_folder_index / target_folder_index?

Separate struct is okay, will do so.

@aome510
Copy link
Owner

aome510 commented Aug 4, 2024

@aome510 for me current_id is misleading because we also have a Spotify id "spotify:user:xxx:folder:yyy". What about current_folder_id / target_folder_id and comment that it's a local id, not just a global Spotify id? Or current_folder_index / target_folder_index?

That also works. I mean Spotify ID doesn't really apply to folder, so we don't really need to differentiate here. Comments/documentations will definitely make it more clear though.

@aNNiMON
Copy link
Contributor Author

aNNiMON commented Aug 5, 2024

@aome510 done

spotify_player/src/client/mod.rs Outdated Show resolved Hide resolved
spotify_player/src/state/model.rs Outdated Show resolved Hide resolved
spotify_player/src/state/model.rs Outdated Show resolved Hide resolved
spotify_player/src/state/model.rs Outdated Show resolved Hide resolved
spotify_player/src/state/ui/popup.rs Outdated Show resolved Hide resolved
spotify_player/src/state/data.rs Outdated Show resolved Hide resolved
spotify_player/src/event/window.rs Outdated Show resolved Hide resolved
spotify_player/src/event/page.rs Outdated Show resolved Hide resolved
spotify_player/src/event/page.rs Outdated Show resolved Hide resolved
spotify_player/src/event/popup.rs Outdated Show resolved Hide resolved
@aome510
Copy link
Owner

aome510 commented Aug 18, 2024

@aNNiMON sorry for the delay (I've been kinda busy lately). I've reviewed the PR again and pushed a commit to simplify the codes and processing logic further. Can you review the commit and test it to ensure that nothing breaks in terms of functionalities? I don't have folders so couldn't really test the feature myself.

@aNNiMON
Copy link
Contributor Author

aNNiMON commented Aug 18, 2024

@aome510, everything works fine. 👍 Thanks

@aome510 aome510 merged commit 2ebc027 into aome510:master Aug 18, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Playlist Folder Support
2 participants