-
Notifications
You must be signed in to change notification settings - Fork 4
Add -f force-import, smart skip, and -D batch import from directory #9
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| """ | ||
|
|
||
| import argparse | ||
| import glob | ||
| import os, sys, random | ||
| import xml.etree.ElementTree as ET | ||
| import soco | ||
|
|
@@ -169,7 +170,45 @@ def exportPl(self, speaker, pl, force, details): | |
| print(pl.title + ': ' + str(cnt) + ' songs') | ||
|
|
||
|
|
||
| def importPl(self, speaker, fileName): | ||
| def getPlUris(self, speaker, pl): | ||
| """ Return an ordered list of track URIs from a live Sonos playlist. """ | ||
| uris = [] | ||
| start = 0 | ||
| max_items = 100 | ||
| try: | ||
| ml = soco.music_library.MusicLibrary(speaker) | ||
| mlAvailable = True | ||
| except: | ||
| mlAvailable = False | ||
| while True: | ||
| if mlAvailable: | ||
| trackList = ml.browse(pl, start=start, max_items=max_items) | ||
| else: | ||
| trackList = speaker.browse(pl, start=start, max_items=max_items) | ||
| if not trackList: | ||
| break | ||
| for item in trackList: | ||
| uris.append(item.resources[0].uri) | ||
| start += max_items | ||
| return uris | ||
|
|
||
|
|
||
| def getFileUris(self, fileName): | ||
| """ Return an ordered list of track URIs from an XSPF file. """ | ||
| uris = [] | ||
| ns = '{http://xspf.org/ns/0/}' | ||
| try: | ||
| with open(fileName, 'r') as fp: | ||
| context = ET.iterparse(fp, events=('end',)) | ||
| for event, elem in context: | ||
| if elem.tag == ns+'location': | ||
| uris.append(unescape(elem.text)) | ||
| except (IOError, ET.ParseError): | ||
| pass | ||
| return uris | ||
|
|
||
|
|
||
| def importPl(self, speaker, fileName, force=False): | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The updates change importPl() to read the playlist file in the middle of reading the playlist file, so the playlist file is read twice. The playlist file should only be read once. I would suggest that importPl() no longer opens the playlist file but relies on getFileUris() to do that. getFileUris() can return the playlist name as well as the URIs. importPl() can use this information to process the playlist. |
||
| """ Import playlist from file to speaker. """ | ||
| try: | ||
| with open(fileName, 'r') as fp: | ||
|
|
@@ -191,15 +230,51 @@ def importPl(self, speaker, fileName): | |
| if elem.tag == ns+'title' and event == 'start': | ||
| #print(elem.text) | ||
| plName = elem.text | ||
| foundIt = False | ||
| existingPl = None | ||
| for pl in speaker.get_sonos_playlists(): | ||
| if pl.title == plName: | ||
| foundIt = True | ||
| if foundIt: | ||
| print('Error: Sonos playlist "%s" already' | ||
| ' exists. The playlist must be deleted' | ||
| ' before importing.' % plName) | ||
| return | ||
| existingPl = pl | ||
| if existingPl is not None: | ||
| fileUris = self.getFileUris(fileName) | ||
| if not fileUris: | ||
| print('Skipping empty playlist "%s".' | ||
| % plName) | ||
| return | ||
| plUris = self.getPlUris(speaker, existingPl) | ||
| if ([u.lower() for u in fileUris] == | ||
| [u.lower() for u in plUris]): | ||
| print('Playlist "%s" is up to date,' | ||
| ' skipping.' % plName) | ||
| return | ||
| if self.verbose: | ||
| fileSet = set(fileUris) | ||
| plSet = set(plUris) | ||
| only_in_file = fileSet - plSet | ||
| only_in_sonos = plSet - fileSet | ||
| if len(fileUris) != len(plUris): | ||
| print(' URI count: file=%d sonos=%d' | ||
| % (len(fileUris), len(plUris))) | ||
| for u in list(only_in_file)[:3]: | ||
| print(' only in file : %s' % u) | ||
| for u in list(only_in_sonos)[:3]: | ||
| print(' only in sonos: %s' % u) | ||
| # show first positional mismatch | ||
| for i, (f, s) in enumerate( | ||
| zip(fileUris, plUris)): | ||
| if f != s: | ||
| print(' first mismatch at [%d]:' | ||
| % i) | ||
| print(' file : %s' % f) | ||
| print(' sonos: %s' % s) | ||
| break | ||
| if not force: | ||
| print('Error: Sonos playlist "%s" already' | ||
| ' exists. Use -f to replace.' | ||
| % plName) | ||
| return | ||
| speaker.remove_sonos_playlist(existingPl) | ||
| print('Importing "%s": ' % plName, end='') | ||
| sys.stdout.flush() | ||
| state = 'findLocation' | ||
| elif state == 'findLocation': | ||
| if elem.tag == ns+'location' and event == 'end': | ||
|
|
@@ -210,8 +285,10 @@ def importPl(self, speaker, fileName): | |
| print('.', end='') | ||
| sys.stdout.flush() | ||
| speaker.add_uri_to_queue(unescape(elem.text)) | ||
| if not firstTrack: | ||
| print('') | ||
| if firstTrack: | ||
| print('Skipping empty playlist "%s".' % plName) | ||
| return | ||
| print('') | ||
| speaker.create_sonos_playlist_from_queue(plName) | ||
| speaker.clear_queue() | ||
| except ET.ParseError: | ||
|
|
@@ -253,9 +330,13 @@ def __init__(self): | |
| ' Default is ACLT.', | ||
| metavar='DETAILS') | ||
| parser.add_argument('-f', '--force', action='store_true', | ||
| help='Force overwrite of export files.') | ||
| help='Force overwrite of export files and' | ||
| ' replacement of existing playlists on import.') | ||
| parser.add_argument('-i', '--importPlaylistFile', action='append', | ||
| help='Import the playlist file.', metavar='FILE') | ||
| parser.add_argument('-D', '--importDir', | ||
| help='Import all .xspf playlists from a directory.', | ||
| metavar='DIR') | ||
| parser.add_argument('-s', '--speaker', | ||
| help='Speaker name or IP address.') | ||
| parser.add_argument('-P', '--partyModeOn', action='store_true', | ||
|
|
@@ -448,7 +529,17 @@ def __init__(self): | |
| # import a playlist from a file | ||
| if args.importPlaylistFile: | ||
| for file in args.importPlaylistFile: | ||
| self.importPl(cspeaker, file) | ||
| self.importPl(cspeaker, file, args.force) | ||
| exit(0) | ||
|
|
||
| # import all playlists from a directory | ||
| if args.importDir: | ||
| files = sorted(glob.glob(os.path.join(args.importDir, '*.xspf'))) | ||
| if not files: | ||
| print('No .xspf files found in: ' + args.importDir) | ||
| exit(-2) | ||
| for file in files: | ||
| self.importPl(cspeaker, file, args.force) | ||
| exit(0) | ||
|
|
||
| # set the volume | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I think this PR is a good addition to the spl script.