raspberry-pi:music_player2
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
raspberry-pi:music_player2 [2023/04/02 14:25] – Wulf Rajek | raspberry-pi:music_player2 [2024/01/14 00:45] (current) – [Add/Remove Radio Stations] Wulf Rajek | ||
---|---|---|---|
Line 5: | Line 5: | ||
|Raspberry Pi Zero W|| | |Raspberry Pi Zero W|| | ||
|Pimoroni Pirate Audio (Headphone)|https:// | |Pimoroni Pirate Audio (Headphone)|https:// | ||
- | |Pimoroni Pirate Audio Case|https:// | + | |Pimoroni Pirate Audio Case w/ Buttons|https:// |
+ | |Pimoroni Pirate Audio Case orig w/o Buttons|https:// | ||
+ | |Pimoroni Pirate Audio Case angled base|https:// | ||
+ | |Pimoroni Pirate Audio Case open angled base|https:// | ||
|Alternative case ideas|https:// | |Alternative case ideas|https:// | ||
|External passive speakers with 3.5mm plug|| | |External passive speakers with 3.5mm plug|| | ||
|3.5mm Headphone Volume Control|https:// | |3.5mm Headphone Volume Control|https:// | ||
- | |Buttons (need case and button adjustment)|https://www.tinkercad.com/ | + | {{raspberry-pi:pirateaudio_with_buttons_base_-_no_holes.stl}} |
- | ===== System installation 8.0.2===== | + | {{raspberry-pi: |
+ | {{raspberry-pi: | ||
+ | |||
+ | ===== System installation 8.3.0 ===== | ||
Download Moode ISO https:// | Download Moode ISO https:// | ||
- | < | + | < |
Main install instructions with reference to auto-install: | Main install instructions with reference to auto-install: | ||
Line 202: | Line 208: | ||
cover = Image.open(cp) | cover = Image.open(cp) | ||
return cover | return cover | ||
- | except | + | except: |
pass | pass | ||
return cover | return cover | ||
Line 498: | Line 504: | ||
===== Add/Remove Radio Stations ===== | ===== Add/Remove Radio Stations ===== | ||
+ | |||
+ | GUI interface to add radio stations via + icon in radio interface. | ||
Radio stations are stored as .pls files in / | Radio stations are stored as .pls files in / | ||
- | ==== MPD Playlist Folder ==== | + | Station logos are stored with same name as pls file in: |
+ | < | ||
+ | / | ||
+ | / | ||
+ | / | ||
+ | </ | ||
+ | < | ||
+ | file "/ | ||
+ | / | ||
+ | |||
+ | file "/ | ||
+ | / | ||
+ | |||
+ | file "/ | ||
+ | / | ||
+ | </ | ||
+ | |||
+ | Example pls file: | ||
+ | <code - / | ||
+ | [playlist] | ||
+ | File1=http:// | ||
+ | Title1=Absolute Classic Rock | ||
+ | Length1=-1 | ||
+ | NumberOfEntries=1 | ||
+ | Version=2 | ||
+ | </ | ||
+ | |||
+ | ===== MPD Playlist Folder | ||
/ | / | ||
Line 538: | Line 573: | ||
</ | </ | ||
+ | ===== MPD Dynamic Playlist ===== | ||
+ | |||
+ | https:// | ||
+ | |||
+ | I present to you, fellow Archers, two little programs. One I've had for months but haven' | ||
+ | |||
+ | MPDDP: MPD Dynamic Playlists is a program to generate, as the name would imply, a dynamic playlist for MPD. You specify rules about what tracks you want added, and it keeps adding them randomly. | ||
+ | |||
+ | Source: | ||
+ | < | ||
+ | # | ||
+ | |||
+ | # MPDDP: MPD Dynamic Playlists | ||
+ | # Call this and run it in the background (eg mpddp &>/ | ||
+ | # Configured in / | ||
+ | |||
+ | import mpd, random, os, time, sys, string | ||
+ | |||
+ | client = mpd.MPDClient() | ||
+ | |||
+ | host = "" | ||
+ | port = 0 # The port MPD is operating upon | ||
+ | |||
+ | playlistlen | ||
+ | changeafter | ||
+ | clearinitially = '' | ||
+ | saveonquit | ||
+ | update | ||
+ | |||
+ | confdir = '/ | ||
+ | savedir = '' | ||
+ | |||
+ | alltracks = [] # All the tracks that can be played. | ||
+ | oldconfig = [] # The configuration as it was last loaded. | ||
+ | |||
+ | def pickNewTrack(): | ||
+ | global client | ||
+ | global alltracks | ||
+ | |||
+ | index = random.randint(0, | ||
+ | track = alltracks[index] | ||
+ | |||
+ | return track | ||
+ | |||
+ | def addNewTrackToPlaylist(): | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | global playlistlen | ||
+ | | ||
+ | client.connect(host, | ||
+ | playlist = client.playlistinfo() | ||
+ | client.disconnect() | ||
+ | |||
+ | if len(playlist) < playlistlen: | ||
+ | track = pickNewTrack() | ||
+ | |||
+ | print " | ||
+ | | ||
+ | client.connect(host, | ||
+ | client.add(track) | ||
+ | client.disconnect() | ||
+ | |||
+ | def removeLastTrackFromPlaylist(): | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | | ||
+ | client.connect(host, | ||
+ | playlist = client.playlistinfo() | ||
+ | client.delete(0) | ||
+ | client.disconnect() | ||
+ | |||
+ | print " | ||
+ | |||
+ | def checkMPDPlaylist(): | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | global playlistlen | ||
+ | global alltracks | ||
+ | | ||
+ | client.connect(host, | ||
+ | playlist = client.playlistinfo() | ||
+ | client.disconnect() | ||
+ | |||
+ | if len(playlist) < playlistlen: | ||
+ | while len(playlist) < playlistlen: | ||
+ | addNewTrackToPlaylist() | ||
+ | | ||
+ | def updatePlaylist(): | ||
+ | checkMPDPlaylist() | ||
+ | removeLastTrackFromPlaylist() | ||
+ | addNewTrackToPlaylist() | ||
+ | |||
+ | def getFilenamesFromMPDSPL(expression): | ||
+ | os.system(' | ||
+ | a = open('/ | ||
+ | ot = a.read() | ||
+ | ot = ot.splitlines() | ||
+ | a.close() | ||
+ | os.remove('/ | ||
+ | return ot | ||
+ | |||
+ | def getFilenamesFromMPD(rules): | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | |||
+ | paths = [] | ||
+ | playlists = [] | ||
+ | smarts | ||
+ | nevers | ||
+ | tracks | ||
+ | | ||
+ | for rule in rules: | ||
+ | if rule[0] == ' | ||
+ | paths.append(rule[1]) | ||
+ | elif rule[0] == ' | ||
+ | playlists.append(rule[1]) | ||
+ | elif rule[0] == ' | ||
+ | smarts.append(rule[1]) | ||
+ | elif rule[0] == ' | ||
+ | nevers.append(rule[1]) | ||
+ | | ||
+ | client.connect(host, | ||
+ | | ||
+ | for path in paths: | ||
+ | temptracks = client.search(" | ||
+ | for track in temptracks: | ||
+ | if isinstance(track, | ||
+ | track = track[' | ||
+ | dontadd = False | ||
+ | for never in nevers: | ||
+ | if never in track: | ||
+ | dontadd = True | ||
+ | if dontadd == False and not track in tracks: | ||
+ | tracks.append(track) | ||
+ | |||
+ | for playlist in playlists: | ||
+ | temptracks = client.listplaylist(playlist) | ||
+ | for track in temptracks: | ||
+ | if isinstance(track, | ||
+ | track = track[' | ||
+ | dontadd = False | ||
+ | for never in nevers: | ||
+ | if never in track: | ||
+ | dontadd = True | ||
+ | if dontadd == False and not track in tracks: | ||
+ | tracks.append(track) | ||
+ | |||
+ | for smart in smarts: | ||
+ | temptracks = getFilenamesFromMPDSPL(smart) | ||
+ | for track in temptracks: | ||
+ | dontadd = False | ||
+ | for never in nevers: | ||
+ | if never in track: | ||
+ | dontadd = True | ||
+ | if dontadd == False and not track in tracks: | ||
+ | tracks.append(track) | ||
+ | |||
+ | client.disconnect() | ||
+ | return tracks | ||
+ | |||
+ | def parseConfigIncludes(conf, | ||
+ | outconf = "" | ||
+ | paths = path | ||
+ | | ||
+ | for line in conf: | ||
+ | line = line.split("#" | ||
+ | line = line[0] | ||
+ | line = line.strip() | ||
+ | | ||
+ | if len(line) > 0: | ||
+ | if line[0:7] == ' | ||
+ | toinclude = line[8: | ||
+ | toinclude = toinclude.replace(" | ||
+ | if not toinclude in paths: | ||
+ | paths.append(toinclude) | ||
+ | filehandler = open(toinclude) | ||
+ | newconf = parseConfigIncludes(filehandler, | ||
+ | outconf = outconf + newconf | ||
+ | filehandler.close() | ||
+ | else: | ||
+ | outconf = outconf + line + " | ||
+ | | ||
+ | return outconf | ||
+ | |||
+ | def parseConfigLine(line): | ||
+ | line = line.split("#" | ||
+ | line = line[0] | ||
+ | line = line.strip() | ||
+ | |||
+ | if len(line) > 0: | ||
+ | if (" | ||
+ | pline = line.split(" | ||
+ | parsed = {' | ||
+ | ' | ||
+ | return parsed | ||
+ | elif ":" | ||
+ | pline = line.split(":", | ||
+ | parsed = {' | ||
+ | ' | ||
+ | return parsed | ||
+ | else: | ||
+ | return {' | ||
+ | else: | ||
+ | return {' | ||
+ | |||
+ | def parseConfigFile(): | ||
+ | global confdir | ||
+ | | ||
+ | filehandler = open(confdir) | ||
+ | conf = parseConfigIncludes(filehandler, | ||
+ | output = {' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | ' | ||
+ | | ||
+ | for line in conf.splitlines(): | ||
+ | result = parseConfigLine(line) | ||
+ | if result[' | ||
+ | output[' | ||
+ | elif result[' | ||
+ | if result[' | ||
+ | output[result[' | ||
+ | else: | ||
+ | print " | ||
+ | elif result[' | ||
+ | try: | ||
+ | if (result[' | ||
+ | output[result[' | ||
+ | else: | ||
+ | print " | ||
+ | except TypeError: | ||
+ | print " | ||
+ | elif result[' | ||
+ | output[result[' | ||
+ | |||
+ | filehandler.close() | ||
+ | |||
+ | return output | ||
+ | |||
+ | def loadPlaylistFromSaved(): | ||
+ | global playlistlen | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | global savedir | ||
+ | |||
+ | loaded = [] | ||
+ | | ||
+ | try: | ||
+ | filehandler = open(savedir + ' | ||
+ | for line in filehandler: | ||
+ | loaded.append(line) | ||
+ | filehandler.close() | ||
+ | |||
+ | client.connect(host, | ||
+ | for track in loaded: | ||
+ | track = track.strip() | ||
+ | if len(track) > 0: | ||
+ | try: | ||
+ | client.add(track) | ||
+ | print " | ||
+ | except mpd.CommandError: | ||
+ | print "Error loading", | ||
+ | client.disconnect() | ||
+ | |||
+ | for i in range(len(loaded), | ||
+ | addNewTrackToPlaylist() | ||
+ | except IOError: | ||
+ | for i in range(0, playlistlen): | ||
+ | addNewTrackToPlaylist() | ||
+ | |||
+ | def populateLists(redoing): | ||
+ | global playlistlen | ||
+ | global changeafter | ||
+ | global clearinitially | ||
+ | global client | ||
+ | global host | ||
+ | global port | ||
+ | global alltracks | ||
+ | global saveonquit | ||
+ | global savedir | ||
+ | global update | ||
+ | global oldconfig | ||
+ | | ||
+ | config = parseConfigFile() | ||
+ | rules = config[' | ||
+ | if redoing == False or (redoing == True and not oldconfig == config): | ||
+ | host = config[' | ||
+ | port = config[' | ||
+ | playlistlen | ||
+ | changeafter | ||
+ | clearinitially = config[' | ||
+ | saveonquit | ||
+ | savedir | ||
+ | update | ||
+ | oldconfig | ||
+ | | ||
+ | print " | ||
+ | | ||
+ | tracks = getFilenamesFromMPD(rules) | ||
+ | | ||
+ | if redoing == True: | ||
+ | alltracks = [] | ||
+ | | ||
+ | if redoing == False or not alltracks == tracks: | ||
+ | alltracks = tracks | ||
+ | |||
+ | client.connect(host, | ||
+ | if clearinitially == ' | ||
+ | client.clear() | ||
+ | client.random(0) | ||
+ | client.disconnect() | ||
+ | |||
+ | if redoing == False: | ||
+ | if saveonquit == ' | ||
+ | for i in range(0, playlistlen): | ||
+ | addNewTrackToPlaylist() | ||
+ | else: | ||
+ | loadPlaylistFromSaved() | ||
+ | |||
+ | client.connect(host, | ||
+ | client.play() | ||
+ | client.disconnect() | ||
+ | |||
+ | def dieGracefully(): | ||
+ | global saveonquit | ||
+ | | ||
+ | if saveonquit == ' | ||
+ | try: | ||
+ | os.remove('/ | ||
+ | print " | ||
+ | except OSError: | ||
+ | print "No old playlist to remove." | ||
+ | |||
+ | try: | ||
+ | os.remove('/ | ||
+ | print " | ||
+ | except OSError: | ||
+ | print "No kill file to remove." | ||
+ | |||
+ | print " | ||
+ | filehandler = open(savedir + ' | ||
+ | playlist = client.playlistinfo() | ||
+ | for track in playlist: | ||
+ | print " | ||
+ | filehandler.write(track[' | ||
+ | filehandler.close() | ||
+ | |||
+ | print " | ||
+ | sys.exit() | ||
+ | |||
+ | # Execute the program main loop | ||
+ | populateLists(False) | ||
+ | loops = 0 | ||
+ | try: | ||
+ | while True: | ||
+ | if os.path.exists('/ | ||
+ | dieGracefully() | ||
+ | | ||
+ | client.connect(host, | ||
+ | info = client.currentsong() | ||
+ | status = client.status() | ||
+ | playlist = client.playlistinfo() | ||
+ | client.disconnect() | ||
+ | | ||
+ | if len(info) > 0: | ||
+ | if int(status[' | ||
+ | for i in range(changeafter - 1, int(status[' | ||
+ | updatePlaylist() | ||
+ | if len(playlist) < playlistlen: | ||
+ | for i in range(len(playlist), | ||
+ | addNewTrackToPlaylist() | ||
+ | | ||
+ | if loops == 59: | ||
+ | if update == ' | ||
+ | populateLists(True) | ||
+ | loops = 0 | ||
+ | else: | ||
+ | loops = loops + 1 | ||
+ | | ||
+ | time.sleep(1) | ||
+ | except KeyboardInterrupt: | ||
+ | dieGracefully() | ||
+ | </ | ||
+ | |||
+ | / | ||
+ | < | ||
+ | |||
+ | server | ||
+ | port = 6600 # The port that MPD is operating upon. | ||
+ | playlistlen | ||
+ | changeafter | ||
+ | clearinitially = no # Whether to clear the playlist upon starting or not. | ||
+ | saveonquit | ||
+ | savedir | ||
+ | update | ||
+ | |||
+ | #include / | ||
+ | |||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | </ | ||
+ | |||
+ | And, for reference, here's a small chunk of my config file: | ||
+ | < | ||
+ | # -*-conf-*- | ||
+ | |||
+ | playlistlen | ||
+ | changeafter | ||
+ | # | ||
+ | # | ||
+ | clearinitially = no | ||
+ | |||
+ | # Playlists | ||
+ | # | ||
+ | # | ||
+ | |||
+ | # Musicals | ||
+ | # | ||
+ | #path:Cats Original London Cast/ | ||
+ | # | ||
+ | #path:Les Misérables Complete Symphonic Recording/ | ||
+ | #path:Les Misérables Original Broadway Cast/ | ||
+ | #path:Les Misérables Original Paris Cast/ | ||
+ | #path:Mary Poppins/ | ||
+ | #path:Miss Saigon CSR/ | ||
+ | #path:Miss Saigon (Original London Cast)/ | ||
+ | # | ||
+ | # | ||
+ | #path:Sound of Music 40th Anniversary Special Edition, The/ | ||
+ | </ | ||
+ | |||
+ | MPDSPL: MPD Smart PlayLists makes smart playlists for MPD. See the " | ||
+ | |||
+ | Source: | ||
+ | < | ||
+ | # | ||
+ | |||
+ | # A script to parse the MPD database into a list of dictionaries (or at least, it was going to be before I decided to finish it). | ||
+ | # Now with patronising comments which assume almost no Python knowledge! | ||
+ | |||
+ | # cPickle is a faster version of the pickle library. It is used to save data structures to a file. Like lists and dictionaries. os is needed for file stuff, sys for arguments, and re for regex. | ||
+ | import cPickle, os, sys, re | ||
+ | |||
+ | # Info about new playlists | ||
+ | newname | ||
+ | newrules = [] | ||
+ | |||
+ | # Place to look for the MPD database and config files, and the loaded MPD config (well, only the values useful to us). | ||
+ | confpath = "/ | ||
+ | mpd = {" | ||
+ | |||
+ | # There is an environmental variable XDG_CACHE_HOME which specifies where to save cache files. However, if not set, a default of ~/.cache should be used. | ||
+ | cachehome = os.path.expanduser(os.environ[' | ||
+ | if cachehome == "": | ||
+ | cachehome = os.environ[' | ||
+ | cachepath = cachehome + "/ | ||
+ | |||
+ | # $XDG_DATA_HOME specifies where to save data files. Like a record of playlists which have been created. If unset a default of ~/ | ||
+ | datahome = os.path.expanduser(os.environ[' | ||
+ | if datahome == "": | ||
+ | datahome = os.environ[' | ||
+ | datapath = datahome + "/ | ||
+ | # If the data directory does not exist, create it. | ||
+ | if not os.path.isdir(datapath): | ||
+ | os.mkdir(datapath) | ||
+ | |||
+ | tracks = [] | ||
+ | forceupdate | ||
+ | simpleoutput = False | ||
+ | |||
+ | # A nice little help function. Read on to see how it is called... | ||
+ | def showhelp(): | ||
+ | print " | ||
+ | print "A script to generate smart playlists for MPD. Currently does nothing of use :p\n" | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | print " | ||
+ | sys.exit() | ||
+ | |||
+ | # Parse the rules regex | ||
+ | def parserules(rulestr): | ||
+ | # rules will be our list of rules, bufferstr will be the buffer for our parser, and i will be a counter | ||
+ | rules = [] | ||
+ | bufferstr = "" | ||
+ | i = 0 | ||
+ | |||
+ | # We want to use the same identifiers as the track dictionaries: | ||
+ | keywords = {" | ||
+ | |||
+ | # For every character in rulestr (we do it characterwise, | ||
+ | for c in rulestr: | ||
+ | # Add the character to the buffer | ||
+ | bufferstr += c | ||
+ | |||
+ | # If the buffer matches one of our keywords, we have hit a new rule, and so create a blank dictionary, and clear the buffer. | ||
+ | if bufferstr.strip() in [" | ||
+ | rules.append({" | ||
+ | bufferstr = "" | ||
+ | # If we're at the start of a blank case-insensitive regex, record that, and clear the buffer. | ||
+ | elif bufferstr == " | ||
+ | rules[i][" | ||
+ | bufferstr = "" | ||
+ | # If not, just clear the buffer for the coming regex. | ||
+ | elif bufferstr == " | ||
+ | bufferstr = "" | ||
+ | # If at the end of a regex, stick it all (sans the trailing slash, they' | ||
+ | elif bufferstr[-1] == "/" | ||
+ | rules[i][" | ||
+ | bufferstr = "" | ||
+ | i += 1 | ||
+ | # Get rid of the escape backslash if a forward slash has been used. | ||
+ | elif bufferstr[-1] == "/" | ||
+ | bufferstr[-2] = "" | ||
+ | # If set to ' | ||
+ | elif bufferstr == " | ||
+ | bufferstr = "" | ||
+ | rules[i - 1][" | ||
+ | |||
+ | # This isn't needed. But it makes things faster and allows us to have case insensetivity. | ||
+ | for rule in rules: | ||
+ | regex = None | ||
+ | if rule[" | ||
+ | # If case insensitive, | ||
+ | regex = re.compile(rule[" | ||
+ | else: | ||
+ | regex = re.compile(rule[" | ||
+ | |||
+ | # Overwrite the regex string with the compiled object | ||
+ | rule[" | ||
+ | |||
+ | return rules | ||
+ | |||
+ | # Splitting things up into functions is good :D | ||
+ | def parseargs(): | ||
+ | # global lets us access variables specified outside our function. | ||
+ | global forceupdate | ||
+ | global mpd | ||
+ | global confpath | ||
+ | global cachepath | ||
+ | global newname | ||
+ | global newrules | ||
+ | global simpleoutput | ||
+ | | ||
+ | newarg = 0 | ||
+ | | ||
+ | for argument in sys.argv: | ||
+ | if not newarg == 0: | ||
+ | # We're making a new playlist. If we're only on the first option after -n, that's the name. If the second, that's the description. | ||
+ | if newarg == 2: | ||
+ | newname = argument | ||
+ | elif newarg == 1: | ||
+ | newrules = parserules(argument) | ||
+ | newarg -= 1 | ||
+ | else: | ||
+ | if argument == " | ||
+ | # If a " | ||
+ | forceupdate = True | ||
+ | elif argument[: | ||
+ | # Looks like their db is somewhere other than / | ||
+ | if argument[: | ||
+ | # Python can't work with ~, which has a reasonable chance of being used (eg: ~/ | ||
+ | mpd[" | ||
+ | elif argument[: | ||
+ | mpd[" | ||
+ | elif argument[: | ||
+ | # Silly person, not keeping their cache where XDG says it should be... | ||
+ | if argument[: | ||
+ | cachepath = os.path.expanduser(argument[2: | ||
+ | elif argument[: | ||
+ | cachepath = os.path.expanduser(argument[12: | ||
+ | elif argument[: | ||
+ | # Now any person which this code applies to is just awkward. | ||
+ | if argument[: | ||
+ | confpath = os.path.expanduser(argument[2: | ||
+ | elif argument[: | ||
+ | confpath = os.path.expanduser(argument[11: | ||
+ | elif argument[: | ||
+ | # As is any person to whom this applies... | ||
+ | if argument[: | ||
+ | mpd[" | ||
+ | elif argument[: | ||
+ | mpd[" | ||
+ | elif argument == " | ||
+ | # Do special treatment to the next 2 arguments | ||
+ | newarg = 2 | ||
+ | elif argument == " | ||
+ | # Ooh, this means that (probably) MPDDP is being used! Yay! | ||
+ | simpleoutput = True | ||
+ | elif argument == " | ||
+ | showhelp() | ||
+ | elif not argument == sys.argv[0]: | ||
+ | # Ooh, stderr. I never actually knew how to send stuff through stderr in python. | ||
+ | print >> sys.stderr, " | ||
+ | sys.exit(1) | ||
+ | |||
+ | # A function to parse a MPD database and make a huge list of tracks | ||
+ | def parsedatabase(database): | ||
+ | global tracks | ||
+ | | ||
+ | i = -1 | ||
+ | parsing | ||
+ | |||
+ | for line in database: | ||
+ | # For every line in the database, remove any whitespace at the beginning and end so the script isn't buggered. | ||
+ | line = line.strip() | ||
+ | |||
+ | # If entering a songList, start parsing. If exiting one, stop. Fairly self explanatory. | ||
+ | if not parsing and line == " | ||
+ | parsing = True | ||
+ | elif parsing and line == " | ||
+ | parsing = False | ||
+ | |||
+ | # If we get a line to parse which is not a " | ||
+ | if parsing and not line == " | ||
+ | if line[0:5] == "key: ": | ||
+ | i += 1 | ||
+ | # Increment the counter and make an empty dictionary if we hit the beginning of a track | ||
+ | tracks.append({" | ||
+ | |||
+ | # Split the line by the first ": ", the string MPD uses, and stick the second part (the value) in the bit of the dictionary referred to by the first part (the key) | ||
+ | splitted = line.split(": | ||
+ | tracks[i][splitted[0]] = splitted[1] | ||
+ | |||
+ | # Grabbing stuff from the MPD config, a very important step | ||
+ | def parsempdconf(): | ||
+ | global confpath | ||
+ | global mpd | ||
+ | | ||
+ | config | ||
+ | # Don't load the user or db_file values if they' | ||
+ | holduser = not mpd[" | ||
+ | holddb | ||
+ | |||
+ | for line in config: | ||
+ | line = line.strip() | ||
+ | if line[:15] == " | ||
+ | rest = line[15: | ||
+ | mpd[" | ||
+ | elif line[:18] == " | ||
+ | rest = line[18: | ||
+ | mpd[" | ||
+ | elif line[:7] == " | ||
+ | rest = line[7: | ||
+ | mpd[" | ||
+ | # The rest of the code in this function wouldn' | ||
+ | elif line[:4] == " | ||
+ | rest = line[4: | ||
+ | mpd[" | ||
+ | |||
+ | if mpd[" | ||
+ | mpd[" | ||
+ | |||
+ | homedir = "/ | ||
+ | if homedir == "/ | ||
+ | homedir = "/ | ||
+ | |||
+ | if " | ||
+ | mpd[" | ||
+ | if " | ||
+ | mpd[" | ||
+ | if " | ||
+ | mpd[" | ||
+ | |||
+ | def findtracks(): | ||
+ | global tracks | ||
+ | global newrules | ||
+ | | ||
+ | # matchingtracks will hold all tracks which match all of the criteria. | ||
+ | matchingtracks = [] | ||
+ | | ||
+ | for track in tracks: | ||
+ | # Initially assume a track *will* be added. | ||
+ | addtrack = True | ||
+ | | ||
+ | for rule in newrules: | ||
+ | # For every track, check it with every rule | ||
+ | if rule[" | ||
+ | if not re.search(rule[" | ||
+ | # If the regular expression matches the track, do not add it to the matchingtracks list. | ||
+ | addtrack = False | ||
+ | else: | ||
+ | if re.search(rule[" | ||
+ | # If the regular expression does not match the track, do not add it to the matchingtracks list. | ||
+ | addtrack = False | ||
+ | | ||
+ | if addtrack: | ||
+ | # Add the track if appropriate | ||
+ | matchingtracks.append(track) | ||
+ | |||
+ | return matchingtracks | ||
+ | |||
+ | def genplaylist(tracks): | ||
+ | global mpd | ||
+ | # Parse a list of track dictionaries into a playlist. Thankfully, m3u is a *very* simple format. | ||
+ | playlist = "" | ||
+ | | ||
+ | for track in tracks: | ||
+ | playlist += mpd[" | ||
+ | |||
+ | return playlist | ||
+ | |||
+ | # Save some random gubbage to a file | ||
+ | def savegubbage(data, | ||
+ | if not os.path.isdir(os.path.dirname(path)): | ||
+ | os.mkdir(os.path.dirname(path)) | ||
+ | |||
+ | # Open the file for writing in binary mode | ||
+ | outfile = open(path, " | ||
+ | # Send the stuff to the file with the magic of cPickle | ||
+ | cPickle.dump(data, | ||
+ | # Close the file handler. Tidy u[p. | ||
+ | outfile.close() | ||
+ | |||
+ | # We might be running as someone other than the user, so make the file writable | ||
+ | os.chmod(path, | ||
+ | |||
+ | def loadgubbage(path): | ||
+ | infile = open(path, " | ||
+ | data = cPickle.load(infile) | ||
+ | infile.close() | ||
+ | |||
+ | return data | ||
+ | |||
+ | def saveplaylist(): | ||
+ | global newname | ||
+ | global newrules | ||
+ | global mpd | ||
+ | global datapath | ||
+ | global simpleoutput | ||
+ | | ||
+ | matchingtracks = findtracks() | ||
+ | playlist = genplaylist(matchingtracks) | ||
+ | |||
+ | if simpleoutput: | ||
+ | for track in matchingtracks: | ||
+ | print track[" | ||
+ | else: | ||
+ | print " | ||
+ | | ||
+ | # Write the contents of the playlist to the m3u file | ||
+ | newlist = open(mpd[" | ||
+ | newlist.write(playlist) | ||
+ | newlist.close() | ||
+ | | ||
+ | # Save as list object. This lets us load them all into a big list nicely. | ||
+ | savegubbage([newname, | ||
+ | |||
+ | |||
+ | # Parse some options! | ||
+ | parseargs() | ||
+ | parsempdconf() | ||
+ | |||
+ | # Check that the database is actually there before attempting to do stuff with it. | ||
+ | if not os.path.exists(mpd[" | ||
+ | print >> sys.stderr, "The database file '" | ||
+ | sys.exit(1) | ||
+ | |||
+ | # If the cache file does not exist OR the database has been modified since the cache file has this has the side-effect of being able to touch the cache file to stop it from being updated. Good thing we have the -f option for any accidental touches (or if you copy the cache to a new location). | ||
+ | if not os.path.exists(cachepath) or os.path.getmtime(mpd[" | ||
+ | if not simpleoutput: | ||
+ | print " | ||
+ | |||
+ | # If the cache directory does not exist, create it. The dirname function just removes the "/ | ||
+ | if not os.path.isdir(os.path.dirname(cachepath)): | ||
+ | os.mkdir(os.path.dirname(cachepath)) | ||
+ | |||
+ | database = open(mpd[" | ||
+ | |||
+ | # Now, parse that database! | ||
+ | parsedatabase(database) | ||
+ | | ||
+ | # Save the parsed stuff to the cache file and close the database file handler. That's not strictly required, python will clean up when the script ends, but you can't unmount volumes with file handlers pointing to them, so it makes a mess. | ||
+ | savegubbage(tracks, | ||
+ | | ||
+ | database.close() | ||
+ | | ||
+ | if not simpleoutput: | ||
+ | # Let's update those playlists! | ||
+ | playlistfiles = os.listdir(datapath) | ||
+ | playlists | ||
+ | | ||
+ | for playlistfile in playlistfiles: | ||
+ | playlists.append(loadgubbage(datapath + "/" | ||
+ | | ||
+ | # Backup the values first. | ||
+ | oldnewname | ||
+ | oldnewrules = newrules | ||
+ | | ||
+ | # Now regenerate! | ||
+ | for playlist in playlists: | ||
+ | newname | ||
+ | newrules = playlist[1] | ||
+ | saveplaylist() | ||
+ | |||
+ | # And restore. | ||
+ | newname | ||
+ | newrules = oldnewrules | ||
+ | else: | ||
+ | # Oh, goodie, we don't need to go through all that arduous parsing as we have a valid cache file :D | ||
+ | if not simpleoutput: | ||
+ | print " | ||
+ | # Open it for reading, load the stuff in the file into the tracks list, close the file handler, and have a party. | ||
+ | tracks = loadgubbage(cachepath) | ||
+ | | ||
+ | # See if we're making a new playlist or not | ||
+ | if not newname == "": | ||
+ | # We are, go go go! | ||
+ | saveplaylist() | ||
+ | </ | ||
+ | |||
+ | The source is full of simple comments because I was supposed to be helping a friend who's less proficient in Python make it but… I got bored of waiting tongue | ||
+ | If your MPD playlists directory is somewhere which you don't have write access to, run it with `sudo -E` | ||
+ | |||
+ | If you need any help with either, ask and ye shall receive. If what you need help with is covered in this post, mpdspl -h, or / | ||
+ | |||
+ | ===== Github projects ===== | ||
+ | |||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | https:// | ||
+ | |||
+ | https:// |
raspberry-pi/music_player2.1680441912.txt.gz · Last modified: 2023/05/29 11:53 (external edit)