def test_smartSyncDelAndShuffle(self): cfg.logger.info("Running test_smartSyncDelAndShuffle") shuffle(self.plPath) currentDir = getLocalSongs(self.plPath) #deletes a third (rounded down) of the songs in the playlist toDelete = [] for _ in range(int(len(currentDir) / 3)): randSong = random.choice(currentDir) while randSong in toDelete: #ensures we dont try to delete the same song twice randSong = random.choice(currentDir) toDelete.append(randSong) for song in toDelete: os.remove(f'{self.plPath}/{song}') smartSync(self.plPath) with shelve.open(f"{self.plPath}/{cfg.metaDataName}", 'c', writeback=True) as metaData: passed = metaDataMatches(metaData, self.plPath) self.assertTrue(passed)
def metaDataSongsCorrect(metaData, plPath): ''' tests if metadata ids corrispond to the correct remote songs. returns boolian test result and logs details test fails if local title does not match remote title manually added songs are ignored ''' cfg.logger.info("Testing if Metadata IDs Corrispond to Correct Songs") currentDir = getLocalSongs(plPath) localIds = metaData['ids'] localTitles = map(lambda title: re.sub(cfg.filePrependRE, '', title), currentDir) for i, localTitle in enumerate(localTitles): localId = localIds[i] if localId != cfg.manualAddId: remoteTitle = getTitle( f"https://www.youtube.com/watch?v={localId}") if localTitle != remoteTitle: message = (f"{i}th Local Title: {localTitle}\n" f"Differes from Remote Title: {remoteTitle}\n" f"With same Id: {localId}") cfg.logger.error(message) return False cfg.logger.info("Test Passed") return True
def getPlaylistData(name): '''used to validate playlist returns list of tups (id, song name)''' result = [] songs = getLocalSongs(f"{cfg.testPlPath}/{name}") with shelve.open(f"{cfg.testPlPath}/{name}/{cfg.metaDataName}", 'c', writeback=True) as metaData: for i, songId in enumerate(metaData['ids']): result.append((songId, songs[i])) return result
def move(plPath, currentIndex, newIndex): if currentIndex==newIndex: cfg.logger.info("Indexes Are the Same") return correctStateCorruption(plPath) currentDir = getLocalSongs(plPath) with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c',writeback=True) as metaData: idsLen = len(metaData["ids"]) numDigits = getNumDigets(idsLen) if currentIndex>=idsLen: cfg.logger.error(f"No song has Index {currentIndex}, Largest Index is {idsLen-1}") return elif currentIndex<0: cfg.logger.error(f"No Song has a Negative Index") return #clamp newIndex if newIndex > idsLen-1: newIndex = idsLen-1 elif newIndex < 0: newIndex = 0 cfg.logger.info(f"Moving {currentDir[currentIndex]} to Index {newIndex}") #moves song to end of list tempName = relabel(metaData,cfg.logger.debug,plPath,currentDir[currentIndex],currentIndex,idsLen,numDigits) if currentIndex>newIndex: #shifts all songs from newIndex to currentIndex-1 by +1 for i in reversed(range(newIndex,currentIndex)): oldName = currentDir[i] relabel(metaData,cfg.logger.debug,plPath,oldName,i,i+1,numDigits) else: #shifts all songs from currentIndex+1 to newIndex by -1 for i in range(currentIndex+1,newIndex+1): oldName = currentDir[i] relabel(metaData,cfg.logger.debug,plPath,oldName,i,i-1,numDigits) #moves song back relabel(metaData,cfg.logger.debug,plPath,tempName,idsLen,newIndex,numDigits) del metaData['ids'][idsLen]
def _removeGaps(plPath): currentDir = getLocalSongs(plPath) numDidgets = len(str(len(currentDir))) for i, oldName in enumerate(currentDir): newPrepend = f"{createNumLabel(i,numDidgets)}_" oldPrepend = re.search(cfg.filePrependRE, oldName).group(0) if oldPrepend != newPrepend: newName = re.sub(cfg.filePrependRE, f"{createNumLabel(i,numDidgets)}_", oldName) cfg.logger.debug(f"Renaming {oldName} to {newName}") os.rename(f"{plPath}/{oldName}", f"{plPath}/{newName}") cfg.logger.debug("Renaming Complete")
def editPlaylist(plPath, newOrder, deletions=False): ''' metaData is json as defined in newPlaylist newOrder is an ordered list of tuples (Id of song, where to find it ) the "where to find it" is the number in the old ordering (None if song is to be downloaded) note if song is in playlist already the id of song in newOrder will not be used ''' currentDir = getLocalSongs(plPath) numDigets = len( str(2 * len(newOrder)) ) #needed for creating starting number for auto ordering ie) 001, 0152 # len is doubled because we will be first numbering with numbers above the # so state remains recoverable in event of crash with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c', writeback=True) as metaData: idsLen = len(metaData['ids']) cfg.logger.info(f"Editing Playlist...") cfg.logger.debug(f"Old Order: {metaData['ids']}") for i in range(len(newOrder)): newId, oldIndex = newOrder[i] newIndex = idsLen + i # we reorder the playlist with exclusivly new numbers in case a crash occurs if oldIndex == None: # must download new song download(metaData, plPath, newId, newIndex, numDigets) else: #song exists locally, but must be reordered/renamed oldName = currentDir[oldIndex] relabel(metaData, cfg.logger.debug, plPath, oldName, oldIndex, newIndex, numDigets) if deletions: oldIndices = [item[1] for item in newOrder] with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c', writeback=True) as metaData: for i in reversed(range(len(currentDir))): if i not in oldIndices: delete(metaData, plPath, currentDir[i], i) _checkBlanks(plPath) _removeGaps(plPath)
def metaDataMatches(metaData, plPath): ''' metadata and local playlist must perfectly match remote playlist for this to return true ''' cfg.logger.info("Testing If Metadata Perfectly Matches Remote Playlist") currentDir = getLocalSongs(plPath) localIds = metaData['ids'] remoteIds, remoteTitles = getIdsAndTitles(metaData['url']) if len(localIds) != len(currentDir): cfg.logger.error( f"metadata ids and local playlist differ in length {len(localIds)} to {len(currentDir)}" ) return False if len(localIds) != len(remoteIds): cfg.logger.error( f"local and remote playlists differ in length {len(localIds)} to {len(remoteIds)}" ) return False for i, localTitle in enumerate(currentDir): localTitle, _ = os.path.splitext(localTitle) localTitle = re.sub(cfg.filePrependRE, '', localTitle) localId = localIds[i] remoteId = remoteIds[i] remoteTitle = remoteTitles[i] if localId != remoteId: message = (f"{i}th Local id: {localId}\n" f"With title: {localTitle}\n" f"Differes from Remote id: {remoteId}\n" f"With title: {remoteTitle}") cfg.logger.error(message) return False if localTitle != remoteTitle: message = (f"{i}th Local Title: {localTitle}\n" f"With Id: {localId}\n" f"Differes from Remote Title: {remoteTitle}\n" f"With Id: {localId}") cfg.logger.error(message) return False return True
def manualAdd(plPath, songPath, posistion): '''put song in posistion in the playlist''' if not os.path.exists(songPath): cfg.logger.error(f'{songPath} Does Not Exist') return correctStateCorruption(plPath) currentDir = getLocalSongs(plPath) with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c',writeback=True) as metaData: idsLen = len(metaData["ids"]) numDigits = getNumDigets(idsLen) #clamp posistion if posistion > idsLen: posistion = idsLen elif posistion < 0: posistion = 0 cfg.logger.info(f"Adding {ntpath.basename(songPath)} to {ntpath.basename(plPath)} in Posistion {posistion}") #shifting elements for i in reversed(range(posistion, idsLen)): oldName = currentDir[i] newName = re.sub(cfg.filePrependRE, f"{createNumLabel(i+1,numDigits)}_" , oldName) with noInterrupt: rename(metaData,cfg.logger.debug,plPath,oldName,newName,i+1,metaData["ids"][i]) metaData["ids"][i] = '' #wiped in case of crash, this blank entries can be removed restoring state newSongName = f"{createNumLabel(posistion,numDigits)}_" + ntpath.basename(songPath) with noInterrupt: os.rename(songPath,f'{plPath}/{newSongName}') if posistion >= len(metaData["ids"]): metaData["ids"].append(cfg.manualAddId) else: metaData["ids"][posistion] = cfg.manualAddId
def showPlaylist(plPath, lineBreak='', urlWithoutId = "https://www.youtube.com/watch?v="): ''' printer can be print or some level of cfg.logger lineBreak can be set to newline if you wish to format for small screens urlWithoutId is added if you wish to print out all full urls ''' with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c',writeback=True) as metaData: cfg.logger.critical(f"Playlist URL: {metaData['url']}") currentDir = getLocalSongs(plPath) if urlWithoutId != None: spacer=' '*(len(urlWithoutId)+11) cfg.logger.critical(f"i: ID{spacer}{lineBreak}-> Local Title{lineBreak}") for i,songId in enumerate(metaData['ids']): url = f"{urlWithoutId}{songId}" cfg.logger.critical(f"{i}: {url}{lineBreak} -> {currentDir[i]}{lineBreak}")
def swap(plPath, index1, index2): '''moves song to provided posistion, shifting all below it down''' if index1 == index2: cfg.logger.info(f"Given Index are the Same") correctStateCorruption(plPath) currentDir = getLocalSongs(plPath) with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c',writeback=True) as metaData: idsLen = len(metaData["ids"]) numDigits = getNumDigets(idsLen) if index1>=idsLen or index2>=idsLen: cfg.logger.error(f"Given Index is Larger than Max {idsLen-1}") return elif index1<0 or index2<0: cfg.logger.error(f"Given Index is Negative") return cfg.logger.info(f"Swapping {currentDir[index1]} and {currentDir[index2]}") #shift index1 out of the way (to idsLen) oldName = currentDir[index1] tempName = relabel(metaData,cfg.logger.debug,plPath,oldName,index1,idsLen,numDigits) #move index2 to index1's old location oldName = currentDir[index2] relabel(metaData,cfg.logger.debug,plPath,oldName,index2,index1,numDigits) #move index1 (now =idsLen) to index2's old location oldName = tempName relabel(metaData,cfg.logger.debug,plPath,oldName,idsLen,index2,numDigits) del metaData["ids"][idsLen]
def updateSongs(self, plPath): self.clear_widgets() localSongs = getLocalSongs(plPath) for i, song in enumerate(localSongs): self.add_widget(DragLabel(self, i, text=song))
def moveRange(plPath, start, end, newStart): ''' moves block of songs from start to end indices, to newStart ie) start = 4, end = 6, newStart = 2 0 1 2 3 4 5 6 7 -> 0 1 4 5 6 2 3 ''' correctStateCorruption(plPath) if start == newStart: return currentDir = getLocalSongs(plPath) with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c',writeback=True) as metaData: idsLen = len(metaData["ids"]) numDigits = getNumDigets(idsLen) if start>=idsLen: cfg.logger.error(f"No song has Index {start}, Largest Index is {idsLen-1}") return elif start<0: cfg.logger.error(f"No Song has a Negative Index") return #clamp end index if end>=idsLen or end == -1: end = idsLen-1 elif end<=start: cfg.logger.error("End Index Must be Greater Than Start Index (or -1)") return #clamp newStart if newStart > idsLen: newStart = idsLen elif newStart < -1: newStart = -1 # Sanatization over # number of elements to move blockSize = end-start+1 # make room for block for i in reversed(range(newStart+1,idsLen)): oldName = currentDir[i] newIndex =i+blockSize relabel(metaData,cfg.logger.debug,plPath,oldName,i,newIndex,numDigits) #accounts for block of songs being shifted if start>newStart offset = 0 if start>newStart: currentDir = getLocalSongs(plPath) offset = blockSize # shift block into gap made for i,oldIndex in enumerate(range(start,end+1)): oldName = currentDir[oldIndex] newIndex = i + newStart+1 relabel(metaData,cfg.logger.debug,plPath,oldName,oldIndex+offset,newIndex,numDigits) # remove number gap in playlist and remove blanks in metadata correctStateCorruption(plPath)
def _checkDeletions(plPath): ''' checks if metadata has songs that are no longer in directory ''' currentDir = getLocalSongs(plPath) with shelve.open(f"{plPath}/{cfg.metaDataName}", 'c', writeback=True) as metaData: idsLen = len(metaData['ids']) # there have been no deletions, however there may be a gap in numberings # which would be falsely detected as a deletion if this check wheren't here if idsLen == len(currentDir): return #song numbers in currentDir currentDirNums = [ int(re.match(cfg.filePrependRE, song).group()[:-1]) for song in currentDir ] numRange = range(idsLen) #difference between whats in the folder and whats in metadata deleted = [i for i in numRange if i not in currentDirNums] numDeleted = len(deleted) if numDeleted > 0: cfg.logger.debug( f"songs numbered {deleted} are no longer in playlist") numDidgets = len(str(len(metaData["ids"]) - numDeleted)) newIndex = 0 for newIndex, oldIndex in enumerate(currentDirNums): if newIndex != oldIndex: oldName = currentDir[newIndex] newName = re.sub( cfg.filePrependRE, f"{createNumLabel(newIndex,numDidgets)}_", oldName) with noInterrupt: cfg.logger.debug(f"Renaming {oldName} to {newName}") os.rename(f"{plPath}/{oldName}", f"{plPath}/{newName}") if newIndex in deleted: # we only remove the deleted entry from metadata when its posistion has been filled cfg.logger.debug( f"Removing {metaData['ids'][newIndex]} from metadata" ) #need to adjust for number already deleted removedAlready = (numDeleted - len(deleted)) del metaData["ids"][newIndex - removedAlready] del deleted[0] cfg.logger.debug("Renaming Complete") while len(deleted) != 0: index = deleted[0] # we remove any remaining deleted entries from metadata # note even if the program crashed at this point, running this fuction # again would yeild an uncorrupted state removedAlready = (numDeleted - len(deleted)) with noInterrupt: cfg.logger.debug( f"Removing {metaData['ids'][index - removedAlready]} from metadata" ) del metaData["ids"][index - removedAlready] del deleted[0]