cool tech zone zone
Playlist Maker - Printable Version

+- cool tech zone zone (https://forum.cooltech.zone)
+-- Forum: Tangara (https://forum.cooltech.zone/forumdisplay.php?fid=3)
+--- Forum: General (https://forum.cooltech.zone/forumdisplay.php?fid=5)
+--- Thread: Playlist Maker (/showthread.php?tid=135)



Playlist Maker - tofuvavohu - 2025-02-07

I really hate databasing a music collection because of the lack of control. I started liking digital music so much more once I stopped using iTunes and the iPod internal software. It's also pretty critical to not be limited to the formats that one for-profit company deems acceptable.

Most of the time, I want to browse by artist, but there are lots of things that I want to be organized in idiosyncratic ways. For example, I have trouble remembering the names of compilations, and there are many comps I think of as being more connected to the label that released them rather than their name. So even if the database does a reasonably good job of linking compilations, they're still hard for me to find (and I find it annoying to scroll through an artist list that has an entry for every artist that appears one time on one comp I have). Using a directory structure, I can keep things grouped by how I mentally map things, so when I feel like listening to the rebetika comp on Canary, I can go to the Canary Records directory and find it even if I can't remember what it's called. There are also lots of artists that have releases under different names, such as all the different variations of Sun Ra (and the Astro-Infinity Arkestra, and His Myth-Science Arkestra, and His Solar Arkestra, etc,etc) or artists who appear in lots of different bands/small ensembles (jazz people in general, but especially someone like Anthony Braxton) where I want them all grouped. Interpreting this just based on the artist tag on some duet or trio albums is not going to work because the main person I'm interested in might not be the first one credited. There are also things like the Harry Smith compilations, where his name does not appear on it at all, but that's how I think of them.

All of this is to say that I can only really use my collection when I can maintain how it's structured. Due to the size of my library, indexing for the database was taking forever, and FLAC files were unplayable until I forced it to stop indexing. Since the file browser isn't sorted, that wasn't a viable option either. I saw some good suggestions on how to create playlists, but doing it manually would take forever, and it really needed to be done recursively. To that end, I wrote something to do the work of generating and sorting playlists to keep the navigation structure and play the files in their proper order. Just to be completely transparent, I have never posted code that I've written online before. I have an art background and to say I have a dabbling experience with programming is quite generous (I think this is the first time I've done something recursive, for instance). There are probably things in the code that are wholly ignorant of best practices and would never be done by someone who actually knows what they're doing and not just furiously paging through an O'Reilly book and typing things into a search engine. I am sharing it in the hope that it will be helpful to other people. I know that by posting it here, it will be seen by people who are legitimate geniuses with software, so please be kind.

This is being shared with no guarantee it will work on your system or with your files/file structure. For reference, I am running Python 3.8.5 on MacOS 15.1. If you see anything that could be improved, please try it out and let me know.

A few notes:
1) By default, it assumes the source file is in the root directory, and that you have a /Music directory that you're reading from and a /Playlists that you're writing to.
   
2) It maintains the same recursive structure of the directories, in other words, you navigate Playlists/Artist/Album/Album.playlist, select that one file, and the album plays.

3) It sorts, but it is not perfect in its sorting:
• It will do alphabetical by capital letters, and then do alphabetical by lowercase letters, so it's best if you're consistent in how your directories are labeled (changing The dB's was a definite pain point in my directory prep...). 
• It doesn't acknowledge articles, so either drop the "The"s, edit to be ", The" at the end, or scroll to T when you want to listen to The Kinks. 
• It's not super graceful with right-to-left languages, but I found by adding extra track numbers to the beginning (or really end) of the filenames solved the issue. I'm guessing most people won't even notice this, and it could also be related to the way I've edited my Bandcamp downloads to not have artist/album info in each filename.

4) It ignores certain files and does not put them in the playlist. The one I was seeing a lot in my testing was hidden files, so it excludes anything that starts with a period. I also have a lot of directories that store cover art or informational items, so it excludes anything that ends with .jpg, .jpeg, .bmp, .png or .pdf. Anything I've ripped with XLD keeps a log, so it ignores anything that ends with .log

5) If you add items and re-run, it skips over any items that already exist. The upside of this is minimizing writes to an SD card. The downside is the Tangara software is going to list the new items at the bottom rather than where they should appear alphabetically. It does create an error log of every playlist it skips, so if you had edited something it should be possible to find if it updated or not. I did one test on moving a playlist from one directory to another, and it didn't bump the edited directory down to the bottom, so maybe that's okay. If in doubt, you can always delete all the playlists and run anew.


Code:
#playlister.py version 1.0

import os

def folder_maker (source_path, destination_path, items, i):
  sub_items=[]
 
  while i < len(items):
    sub_source_path = f'{source_path}/{items[i]}'
    sub_destination_path = f'{destination_path}/{items[i]}'
 
   
    if os.path.isdir(sub_source_path): 
      if not os.path.isdir(sub_destination_path):
        print(f'Creating directory {sub_destination_path}')
        os.mkdir (sub_destination_path)
      else:
        print('Playlist directory exists. Moving on to next item')

      next_level_items= [f for f in os.listdir(sub_source_path) if not f.startswith('.') if not f.endswith('.jpg') if not f.endswith('.jpeg') if not f.endswith('.bmp') if not f.endswith('.png') if not f.endswith('.pdf') if not f.endswith('.log')]
      next_level_items.sort()
      folder_maker (sub_source_path,sub_destination_path,next_level_items, 0)

      i=i+1
 
    if os.path.isfile(sub_source_path):           
      playlist_path ='/' + sub_source_path.lstrip(local_dir)
      sub_items.append(playlist_path)
      sub_items.sort()
     
      i=i+1

    if i is len(items) and sub_items:
      divvied_path=sub_destination_path.rsplit('/',2)
      playlist_name = f'{divvied_path[0]}/{divvied_path[1]}/{divvied_path[1]}.playlist'
     
      if not os.path.isfile(playlist_name):
        print(f'Writing Playlist for {divvied_path[1]} at\n{playlist_name}')
       
        j=0
        while j <len(sub_items):
          with open(playlist_name, 'a', encoding='utf8') as wf:
            wf.write(f'{sub_items[j]}\n')
            j=j+1
      else:
        print('Playlist File exists. See error log')
        with open(error_log, 'a', encoding='utf8') as wf:
          wf.write(f'The playlist file:\n{divvied_path[1]}/{divvied_path[1]}.playlist\nwas already in the playlist directory. It has not been updated, even if the items in the Music directory have changed\n\n')

      i=i+1
 
local_dir=os.path.dirname(os.path.realpath(__file__))

source_folder = f'{local_dir}/Music'
print(f'The source directory for music is located at:\n{source_folder}')

dest_folder = f'{local_dir}/Playlists'
print(f'The destination directory for playlists will be:\n{dest_folder}')

error_log =f'{local_dir}/error_log.txt'
print(f'Playlists that already exist will be printed in the error log\n{error_log}')

source_folder_items = [f for f in os.listdir(source_folder) if not f.startswith('.')]
source_folder_items.sort()
if source_folder_items:
  folder_maker(source_folder,dest_folder,source_folder_items,0)

print (f'\n\n\n\n\n\n\n\nThere were {len(source_folder_items)} items in {source_folder} that were indexed.')

if os.path.isfile(error_log):
  print(f'\nSome playlists already existed. They are listed in \n{error_log}')

.txt   playlister.py.txt (Size: 2.7 KB / Downloads: 4)


RE: Playlist Maker - almond - 2025-02-12

My hero!!!! I was planning on writing a bash script to do the same thing for the exact same reasons, so thanks for saving me the effort :^)


RE: Playlist Maker - taivlam - 2025-03-10

BTW, the official documentation page states that you can make playlists manually via a text file.

This is probably fine if you want to make a playlist with about 30 items, but this would be tedious to make a playlist with over +100 songs.

An alternative tip for the time being: if you use Auxio and you've basically dragged and dropped your music collection into your Tangara's SD card; then you can export any playlist from Auxio in the format of ".m3u" and save it onto your Tangara.


RE: Playlist Maker - taivlam - 2025-03-14

I think I had imported the ".m3u" file with the relative path option in Auxio. This wasn't the exact prefix for the file path names, but I remember I had to delete a prefix like "./..*" and replace it with "Music/*" with a text editor, as described from the "Playlists" page. (Luckily, "find and replace" will help you here.)

A side effect of having incorrect path names for a song ais that certain tracks will make Tangara suddenly stop - and if you're have a queue open with more than 2-3 songs lined up, then the music will suddenly stop. Wait long enough, and Tangara will soft crash and reboot. (I was troubleshooting a playlist, and only had this symptom. After looking at the playlist file again, I realize the path name to the offending song still had a wrong prefix. Correcting this, every song on the playlist worked correctly.)

A small note: the literal order of the imported playlist is the exact order shown in the playlist file. If you want to change up the order, then you'll have to manually rearrange the order with a text editor. Also, currently shuffling a playlist doesn't mean that every song will be played exactly once before songs repeat - this is different than how shuffling is set up in Auxio.


RE: Playlist Maker - ailurux - 2025-03-14

(2025-03-14, 01:22 AM)taivlam Wrote: Also, currently shuffling a playlist doesn't mean that every song will be played exactly once before songs repeat - this is different than how shuffling is set up in Auxio.

IIRC, shuffling will play each song once before it repeats any *but* there are some times where the queue needs to be reshuffled. If any items are added, or as the playlist is still opening, the shuffle will only pick ones that are currently loaded. When all the tracks finish loading into the queue from the playlist, the queue is shuffled properly and from then it should act the way you've described.


RE: Playlist Maker - tofuvavohu - 2025-04-26

I have made some changes to the way this script works after seeing some discussion about sorting from Issue 118 on Codeberg . Ailrux posted about the fatsort utility. This takes a FAT filesystem and puts it in alphabetical order. Since the biggest issue I was solving with this script was that nothing was sorted, this is a fantastic solution to that problem. 

The main drawback on my previous script was that while the separate folder for playlists were navigable, there was no way to look back at the file list. For instance, if you wanted to see what the next or previous track was, you could only skip to it rather than look at a list while the music still played. My new version resolves this by adding the playlists to the directory with the files, so there is only one directory tree to navigate. I left the sorting functions in the script so that it will still properly sequence the playlist, and then after running the script, you run the sorting utility to sequence everything on the card (the playlist names start with "000" now, so it will appear before any of the files assuming that the sorter is run after the script). I used the Mac interface Ailrux linked to, and later explained is just a nice wrapping for the command line program that runs on a variety of *NIXen. Someday Issue 124 might get resolved, which would solve the other issue that my script, but in the meantime this works way better than my previous system, so I thought others might find it useful.

Code:
#playlister.py version 1.2
#This version adds a playlist to each folder in the /Music directory instead of creating. This works as long as the drive is handled by fatsorter

import os

def folder_dive (source_path, items, i):
  sub_items=[]
 
  while i < len(items):
    sub_path = f'{source_path}/{items[i]}'
 
    if os.path.isdir(sub_path): 
      next_level_items= [f for f in os.listdir(sub_path) if not f.startswith('.') if not f.endswith('.jpg') if not f.endswith('.jpeg') if not f.endswith('.bmp') if not f.endswith('.png') if not f.endswith('.pdf') if not f.endswith('.log') if not f.endswith('.cue') if not f.endswith('.txt') if not f.endswith('.txt#') if not f.endswith('.rtf')  if not f.endswith('.md5')]

      next_level_items.sort()
      folder_dive (sub_path, next_level_items, 0)

      i=i+1
 
    if os.path.isfile(sub_path):           
      playlist_path ='/' + sub_path.lstrip(local_dir)
      sub_items.append(playlist_path)
      sub_items.sort()
     
      i=i+1

    if i is len(items) and sub_items:
      divvied_path=sub_path.rsplit('/',2)
      playlist_name = f'{divvied_path[0]}/{divvied_path[1]}/000_{divvied_path[1]}.playlist'
     
      if not os.path.isfile(playlist_name):
        print(f'Writing Playlist for {divvied_path[1]} at\n{playlist_name}')
       
        j=0
        while j <len(sub_items):
          with open(playlist_name, 'a', encoding='utf8') as wf:
            wf.write(f'{sub_items[j]}\n')
            j=j+1

      else:
        print('Playlist File exists. See error log')
        with open(error_log, 'a', encoding='utf8') as wf:
          wf.write(f'The playlist file: {divvied_path[1]}.playlist\nalready existed. It has not been updated, even if the items in {divvied_path[0]}/{divvied_path[1]} have changed.\n\n')

      i=i+1

local_dir=os.path.dirname(os.path.realpath(__file__))

source_folder = f'{local_dir}/Music'
print(f'The source directory for music is located at:\n{source_folder}')

error_log =f'{local_dir}/error_log.txt'
print(f'Playlists that already exist will be printed in the error log\n{error_log}')

source_folder_items = [f for f in os.listdir(source_folder) if not f.startswith('.')]
source_folder_items.sort()

if source_folder_items:
  folder_dive(source_folder,source_folder_items,0)

print (f'\n\n\n\n\n\n\n\nThere were {len(source_folder_items)} items in {source_folder} that were indexed.')

if os.path.isfile(error_log):
  print(f'\nSome playlists already existed. They are listed in \n{error_log}')