-
Notifications
You must be signed in to change notification settings - Fork 0
/
quodlibet_filesorter.py
697 lines (633 loc) · 22.7 KB
/
quodlibet_filesorter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
#!/usr/bin/python3
# -*- coding: utf_8 -*-
import sqlite3
import shutil, re, os, logging
from datetime import datetime
from glob import glob
from sys import stdout, argv
from readtag import get_id3Tag # local library
__version__ = "2.1.0"
#=====================================
# Custom Error Classes
#=====================================
class NotStringError(ValueError):
pass
class MalformedPathError(ValueError):
pass
class EmptyStringError(ValueError):
pass
#=====================================
# Functions
#=====================================
def NoTAlloChReplace (myfilename):
''' This function gets a string and replace a set of characters by a underscore.
It is intended to clean filenames and add compatibility with Windows and OSx file systems
'''
chars = r'\:*?"<>|'
for i in chars:
myfilename = myfilename.replace(i, '_')
return myfilename
def trimto (texto, widht):
textout = texto
if len (texto) > widht:
textout = "..." + texto [-47:]
return textout
def Getchunklist (fgstring, delimitters):
'''Getchunklist
It splits into chunks a string expression due to delimiters
delimieters must be a string of two characters
it returns a list of aplitted texts
example:
Getchunklist ('~/Genre/<artist>/<album>/[<cd> -][<track> -]<title>.<ext>', '[]')
Generated list is:
[
'~/Genre/<artist>/<album>/',
'[<cd> -]',
'[<track> -]',
'<title>.<ext>'
]
'''
chunklist = []
if fgstring == '' or fgstring == None:
return []
from_pos, to_pos, switch = 0,0,0
for char in fgstring:
if char == delimitters[0] and switch == 0:
if from_pos != to_pos:
chunklist.append (fgstring[from_pos:to_pos])
switch = 1
from_pos = to_pos
elif char == delimitters[1] and switch == 1:
chunklist.append (fgstring[from_pos:to_pos + 1])
switch = 0
from_pos = to_pos + 1
to_pos += 1
if from_pos < to_pos:
chunklist.append (fgstring[from_pos:to_pos])
return chunklist
def CharChange (string):
""" Replaces character in a string with assigned values
"""
charset = {
'/\a\b\f\r\v':'',
'\n\t':' ',
'|~%':'-',
}
for a in charset:
for i in a:
string = string.replace (i,charset[a])
return string
def itemcheck(pointer):
''' returns what kind of a pointer is '''
if not (type(pointer) is str):
raise NotStringError ('Bad input, it must be a string')
if pointer.find("//") != -1 :
raise MalformedPathError ('Malformed Path, it has double slashes')
try:
if os.path.isfile(pointer):
return 'file'
except ValueError:
print ('ValueError: embedded null byte')
print ('file:', pointer)
exit()
if os.path.isdir(pointer):
return 'folder'
if os.path.islink(pointer):
return 'link'
return ""
def Nextfilenumber (dest):
''' Returns the next filename counter as filename(nnn).ext
input: /path/to/filename.ext
output: /path/to/filename(n).ext
'''
if dest == "":
raise EmptyStringError ('empty strings as input are not allowed')
filename = os.path.basename (dest)
extension = os.path.splitext (dest)[1]
# extract secuence
expr = r'\(\d{1,}\)' + extension
mo = re.search (expr, filename)
try:
grupo = mo.group()
except:
# print ("No final counter expression was found in %s. Counter is set to 0" % dest)
counter = 0
cut = len (extension)
else:
# print ("Filename has a final counter expression. (n).extension ")
cut = len (mo.group())
countergroup = (re.search (r'\d{1,}', grupo))
counter = int (countergroup.group()) + 1
if cut == 0 :
newfilename = os.path.join( os.path.dirname(dest), filename + "(" + str(counter) + ")" + extension)
else:
newfilename = os.path.join( os.path.dirname(dest), filename [0:-cut] + "(" + str(counter) + ")" + extension)
return newfilename
def Nextplaylistname (dest):
""" It checks and gives a new name to the playlist.
Adds a 0 to the end.
If newer file exists, the adds another 0 and so on
"""
newname = dest+"0"
while itemcheck(newname) != "":
newname = newname+"0"
return newname
def Filemove (origin, dest):
""" Moves files or folders,
it makes necessary directories,
moves folders and subfolders,
it gives a next secuencial number in case of an existing archive
it returns the moved path.
please do not use with symliks.
___dependencies__:
itemcheck (pointer)
Nextfilenumber (dest)
dummy variable
"""
origin_is = itemcheck (origin)
if origin_is not in ('file','folder'):
logging.warning ('!! This should not be here, is a link !!')
return None
while itemcheck (dest) != "":
dest = Nextfilenumber (dest)
logging.info ("\tObject-file already exists at destination, assigning recursively a new name. >> {}".format(dest))
if dest == origin:
logging.info ("\tFound itself while iterating a new name. The file remains.>> {}".format(dest))
return dest
if not dummy:
renameflag = False
if origin.lower() == dest.lower():
renameflag = True
dest = dest + "_"
if origin_is == 'file':
if itemcheck (os.path.dirname(dest)) == '':
os.makedirs (os.path.dirname(dest))
shutil.move (origin, dest)
if renameflag:
shutil.move (dest, dest[:-1])
dest=dest.removesuffix("_")
logging.debug ("\t{} has been moved. {}".format(origin_is,dummymsg))
return dest
def Pathnormalizer (path):
normpath = path
normpath = re.sub ('/+', '/', normpath)
return normpath
class Progresspercent:
''' Show the progression of an activity in percentage
it is swhon on the same line'''
def __init__ (self, maxValue, title = '', showpartial=True):
if title != '':
self.title = f" {title} " # Name of the
else:
self.title = " "
self.maxValue = maxValue
self.partial = showpartial
def showprogress (self, p, valuetext = ""):
'''
Shows the progress in percentage vía stdout, and returns its value again.
'''
progresspercent = '{:.2%}'.format(float(p) / self.maxValue)
if self.partial == True:
progresspartial = '({:6}/{:<6})'.format(p,self.maxValue)
else:
progresspartial = ''
progresstext = f"{self.title}{valuetext}{progresspartial}{progresspercent}"
stdout.write (progresstext + chr(8)*len(progresstext))
if p == self.maxValue:
stdout.write('\n')
stdout.flush()
return progresspercent
def fetchtagline (textfile,tag,sep):
'''Opens a textfile and fetch a value when the tag and sep is encountered
'''
with open (textfile,'r') as f:
for line in f:
if line.startswith(tag):
pos = line.find(sep)
if pos > 0:
value = line[pos+1:].strip()
break
return value
def getmetasep (scanline,sep):
#scanline = scanline
chunklist = list()
chunk = ''
rpos = 0
while rpos < len (scanline):
esc = False
if scanline [rpos] == '\\':
if rpos+1 < len (scanline):
rpos += 1
esc = True
addchar = scanline [rpos]
chunk = chunk + addchar
if not esc and addchar == sep:
chunklist.append(chunk[:-1])
chunk = ''
if rpos+1 == len(scanline):
chunklist.append(chunk)
rpos += 1
return chunklist
def addchilddirectory(directorio):
""" Returns a list of child directories
Usage: addchilddirectory(directory with absolute path)"""
addeddirs = []
ficheros = os.listdir(directorio)
for a in ficheros:
item = os.path.join(directorio, a)
if os.path.isdir(item):
addeddirs.append(item)
return addeddirs
def lsdirectorytree( directory = os.getenv( 'HOME')):
""" Returns a list of a directory and its child directories
usage:
lsdirectorytree ("start directory")
By default, user's home directory
Own start directory is also returned as result
"""
#init list to start, own start directory is included
dirlist = [directory]
#setting the first scan
moredirectories = dirlist
while len (moredirectories) != 0:
newdirectories = moredirectories
moredirectories = list ()
for element in newdirectories:
toadd = addchilddirectory(element)
moredirectories += toadd
dirlist += moredirectories
return dirlist
def getqluserfolder ():
folderlist = [
os.path.join (os.getenv('HOME') ,'.config/quodlibet'),
os.path.join (os.getenv('HOME') ,'.quodlibet'),
]
for f in folderlist:
if itemcheck(f) == 'folder':
return f
else:
print ('no quodlibet config folder was found,\nEnsure that Quodlibet is intalled')
exit()
#=====================================
# User config
#=====================================
userfilegrouppingtag = 'filegroupping' # Tag name which defines the desired path structure for the file
qluserfolder = getqluserfolder()
qlcfgfile = os.path.join (qluserfolder ,'config')
dbpathandname = f'{userfilegrouppingtag}.sqlite3' # Sqlite3 database archive for processing
#filepaths = [os.path.join(os.getenv('HOME'),'Music'), ] # List of initial paths to search.
dummy = False # Dummy mode, True means that the software will check items, but will not perform file movements
dummymsg = ''
#========== Command line options ==========
# for now just one parameter --dummy
try:
if argv[1] == '--dummy':
dummy = True
except:
pass
#========== Dummy message ==========
if dummy:
dummymsg = '(dummy mode)'
print ('** (Running in Dummy mode) **')
#========== Fetch library paths ==========
scanline = fetchtagline (qlcfgfile,'scan','=')
librarypaths = getmetasep (scanline,':')
print (f'libraries to read:{librarypaths}')
#=====================================
# Main
#=====================================
if __name__ == '__main__':
print (f'Running, this could take a while. {dummymsg}')
loginlevel = 'INFO' # INFO ,DEBUG
logpath = './'
logging_file = os.path.join(logpath, f'{userfilegrouppingtag}.log')
# Getting current date and time
now = datetime.now()
today = "/".join([str(now.day), str(now.month), str(now.year)])
tohour = ":".join([str(now.hour), str(now.minute)])
print (f'\tLoginlevel: {loginlevel}')
logging.basicConfig(
level = loginlevel,
format = '%(asctime)s : %(levelname)s : %(message)s',
filename = logging_file,
filemode = 'w' # a = add
)
print (f'\tLogging to: {logging_file}')
logging.info (f'Quodlibet config folder fount at {qluserfolder}')
#initializing DB
if os.path.isfile(dbpathandname):
os.remove (dbpathandname)
con = sqlite3.connect (dbpathandname)
con.execute ("CREATE TABLE SongsTable \
(id INT PRIMARY KEY NOT NULL, \
mountpoint TEXT NOT NULL, \
filefolder TEXT NOT NULL, \
filename TEXT NOT NULL, \
format TEXT NOT NULL, \
fullpathfilename TEXT NOT NULL,\
filegroupping TEXT NOT NULL, \
targetpath TEXT NOT NULL, \
fileflag TEXT NOT NULL)" )
con.execute ('CREATE VIEW "ScannedFolders" AS SELECT DISTINCT filefolder FROM songstable WHERE fullpathfilename <> targetpath')
con.execute ('CREATE VIEW "Allfolders" AS SELECT DISTINCT filefolder FROM songstable')
con.execute ('CREATE TABLE Associatedfiles \
(originfile TEXT NOT NULL, \
targetpath TEXT NOT NULL, \
fileflag TEXT NOT NULL)')
con.execute ('CREATE VIEW "filemovements" AS SELECT * FROM (\
SELECT * FROM \
associatedfiles UNION \
SELECT fullpathfilename as originfile, targetpath, fileflag \
FROM songstable) \
WHERE originfile <> targetpath ORDER BY originfile')
print ('\tScanning librarypaths for mp3 files')
# Iterating over scanned paths
for scanpath in librarypaths:
folders_counter, processed_counter = 0, 0
### iterate over mp3 files. addressing Database
listree = lsdirectorytree (scanpath)
progressindicator = Progresspercent (len(listree), title = '\tScanning files in directories', showpartial=True)
for d in listree:
if "/.Trash-1000/" in d: #is a trash folder, ignoring
continue
itemlist = list()
itemlist += glob(os.path.join(d,'*.mp3'))
itemlist += glob(os.path.join(d,'*.MP3'))
itemlist += glob(os.path.join(d,'*.Mp3'))
if len (itemlist) > 0:
for f in itemlist:
logging.debug ('>>>>')
logging.debug (f'Working on file: {f}')
fullpathfilename = os.path.join (d,f)
audiofile = get_id3Tag (fullpathfilename)
if audiofile == None:
continue
tagvalue = audiofile.readtag (userfilegrouppingtag)
logging.debug (f'\tFilegroupping value:{tagvalue}')
if not (tagvalue == None or tagvalue == ''):
processed_counter += 1
mountpoint = scanpath
filefolder = d
filename = f
extension = os.path.splitext (fullpathfilename)[1]
filegroupping = tagvalue
if filegroupping.endswith ('.<ext>'):
addfilenameflag = False
tmpfilegroupping = filegroupping [:-6] # Deletes '.<ext>' trailing
else:
addfilenameflag = True
tmpfilegroupping = filegroupping
#Splicing filegrouppingtag in chunks
chunklist = Getchunklist (tmpfilegroupping,'[]')
logging.debug ('Chunklist = {}'.format(chunklist))
targetpath = ''
for chunk in chunklist:
# Checking and flagging an optional chunk
optionalflag = False
if chunk.startswith ('[') and chunk.endswith (']'):
optionalflag = True
# We start with the chunk, and later perform tag substitutions
formedchunk = chunk
taglist = re.findall (r'<\w*>',chunk)
logging.debug (f'\tchunk = {chunk}')
logging.debug (f'\ttaglist= {taglist}')
for tag in taglist:
metaname = tag[1:-1] # Eliminates < >
# Break and return an empty chunk if the tag is not present
if metaname not in audiofile.keys() and optionalflag:
formedchunk = ''
break
metavalue = audiofile.readtag (metaname)
# we trim the slash and total tracks if any
if metaname in ('tracknumber','discnumber') :
slashpos = metavalue.find('/')
if slashpos != -1:
metavalue = metavalue [:slashpos]
if metavalue.isdigit():
metavalue = '{:0>2}'.format (metavalue)
#if metavalue.endswith('[Unknown]'):
if metavalue == None:
metavalue = f'[no <{metaname}>]'
logging.debug (f'\t\tmetaname = {metaname}\tmetavalue = {metavalue}')
metavalue = CharChange (metavalue) # clears some non allowed chars
formedchunk = formedchunk.replace('<'+ metaname + '>',metavalue)
if optionalflag and formedchunk != '':
formedchunk = formedchunk [1:-1]
targetpath = targetpath + formedchunk
# Adding a mountpoint lead if necessary
if targetpath.startswith ('~/'):
targetpath = mountpoint + targetpath [1:]
# Relative paths are mounted on current song's mount-point
elif not targetpath.startswith ('/'):
targetpath = os.path.join(mountpoint,targetpath)
# Preserving original filename if could not constructo a valid one.
if targetpath.endswith('/') and not addfilenameflag:
#targetpath = targetpath[:-1]
addfilenameflag = True
logging.warning ('It was not possible construct a valid filename, I will preserve original filename.')
# Adding original filename if necessary
if addfilenameflag:
targetpath = targetpath + os.path.basename(fullpathfilename)
else:
targetpath = targetpath + extension
targetpath = os.path.normpath (targetpath)
targetpath = NoTAlloChReplace (targetpath)
logging.debug ('\ttargetpath = {targetpath}')
valuetuple = ( processed_counter,
mountpoint,
filefolder,
filename,
extension[1:],
fullpathfilename,
filegroupping,
targetpath,
'Qfile'
)
con.execute ("INSERT INTO SongsTable VALUES (?,?,?,?,?,?,?,?,?)", valuetuple)
progressindicator.showprogress (folders_counter); folders_counter += 1
con.commit()
print ('\t{} total songs fetched from librarypaths.'.format(processed_counter))
if processed_counter > 0:
print ('\t{} songs with <{}> tag defined ({:.1%}).'.format(processed_counter, userfilegrouppingtag, float(processed_counter)/processed_counter))
###
### Looking for Associated files and folders
###
logging.debug ('#'*43)
logging.debug ('## Looking for Associated files and folders')
logging.debug ('#'*43)
print ('\tLooking for associated files.')
cursor = con.cursor ()
cursor.execute ('SELECT * FROM ScannedFolders')
for contaninerfolder in cursor:
originfolder = contaninerfolder[0] # SELECT Query returns a tuple ([0],)
if itemcheck (originfolder) != 'folder':
logging.warning('Folder is not present, Skipping!: {}'.format(originfolder))
continue
itemlist = os.listdir (originfolder)
ATargetdict = dict () # Associated target list, and number of leading coincidences.
Aitemdict = dict () # Associated items dictionary
leading_counter = 0 # Leading tracks counter on a folder.
associated_counter = 0 # Associated files counter.
afolder_counter = 0 # Associated folders counter.
for item in itemlist:
typeflag = None
targetfile = None
originfile = os.path.join(originfolder,item)
logging.debug ('')
logging.debug ('originfile = ' + originfile)
if os.path.isfile (originfile):
exist, a_targetfile_path = con.execute ('SELECT COUNT (fullpathfilename), targetpath \
FROM songstable WHERE \
fullpathfilename = ?', (originfile,)).fetchone()
if exist:
logging.debug('\t > file is a leading file.')
a_targetfolder_path = os.path.dirname(a_targetfile_path)
if a_targetfolder_path in ATargetdict:
ATargetdict[a_targetfolder_path] += 1
else:
ATargetdict[a_targetfolder_path] = 1
leading_counter += 1
continue
logging.debug('\t > file is going to be Associated')
associated_counter += 1
typeflag = 'Afile'
elif os.path.isdir (originfile):
exist = con.execute ('SELECT COUNT (filefolder) \
FROM Allfolders \
WHERE filefolder LIKE ?', (originfile+'%',)).fetchone()[0]
if exist:
logging.debug ('\tFolder has some songs to be processed, I will not move this folder: {}'.format(originfile))
continue
logging.debug ('\t > folder is going to be associated: {}'.format(originfile))
afolder_counter += 1
typeflag = 'Afolder'
else:
logging.warning ('This may be a symbolic link. It will be discarded')
continue
Aitemdict [item] = typeflag
## Selecting the most suitable destination
winnerpath = ''
points = 0
for i in ATargetdict:
if ATargetdict[i] > points:
winnerpath, points = i, ATargetdict[i]
## Inserting Associated files into DB
for i in Aitemdict:
valuetuple = (os.path.join(originfolder,i), os.path.join(winnerpath,i), Aitemdict[i])
con.execute ('INSERT INTO associatedfiles VALUES (?,?,?)', valuetuple)
logging.debug('\tleading processed files: {}'.format(leading_counter))
logging.debug('\tassociated processed files: {}'.format(associated_counter))
logging.debug('\tNumber of associated target Paths: {}'.format (len(ATargetdict)))
con.commit()
## Lowering associated file names and extension simplification.
# Lower filenames in targetpath field.
# On SAMBA systems, upper and lowercase is the same file/path
cursor = con.cursor ()
cursor2 = con.cursor ()
cursor.execute ('SELECT * FROM Associatedfiles WHERE fileflag = "Afile" ')
for originfile, targetpath, fileflag in cursor:
#print (originfile, targetpath, fileflag)
Tpath, Tfile = os.path.split(targetpath)
Tfilename, Tfileext = os.path.splitext(Tfile)
Tfilename, Tfileext = Tfilename.lower(), Tfileext.lower()
if Tfileext == ".jpeg":
Tfileext = ".jpg"
newtargetpath = os.path.join(Tpath,Tfilename+Tfileext)
if targetpath != newtargetpath:
cursor2.execute("UPDATE Associatedfiles SET targetpath=? WHERE originfile=?", (newtargetpath, originfile))
con.commit()
# Reporting
T_associated_files = con.execute("SELECT COUNT () FROM filemovements WHERE fileflag = 'Afile'").fetchone()[0]
T_afolder_counter = con.execute("SELECT COUNT () FROM filemovements WHERE fileflag = 'Afolder'").fetchone()[0]
pluralfi, pluralfo = 's','s'
if T_associated_files == 1: pluralfi = ''
if T_afolder_counter == 1: pluralfo = ''
print (f'\t\t{T_associated_files} associated file{pluralfi} found.')
print (f'\t\t{T_afolder_counter} associated folder{pluralfo} found.')
###
### File operations
###
total = cursor.execute ("SELECT COUNT () FROM filemovements where originfile not like '%/.Trash-%'").fetchone()[0]
progressindicator = Progresspercent (total, title = '\tMoving files', showpartial=True)
counter = 1
cursor.execute ("SELECT * FROM filemovements WHERE originfile NOT LIKE '%/.Trash-%'")
print ('\tPerforming file operations.')
for origin, dest, fileflag in cursor:
progressindicator.showprogress (counter); counter += 1
logging.debug (f'\t {fileflag} from: {origin}')
if itemcheck (origin) == '':
loggingmsg = f'** Warning, {fileflag} at {trimto(origin,20)} does not exist. Skipping'
print (loggingmsg)
logging.warning (loggingmsg)
continue
movedto = Filemove (origin, dest)
logging.debug (f'\t file moved: {dest}')
logging.debug ('')
###
### fixing playlists
###
print ('\tChecking Playlists')
# Create playlist DataBase and View
con.execute ("CREATE TABLE Playlists \
(id INT PRIMARY KEY NOT NULL, \
playlistfile TEXT NOT NULL, \
position INT NOT NULL, \
originfile TEXT NOT NULL)" )
con.execute ("CREATE VIEW Playlists_new AS SELECT p.*, s.targetpath FROM Playlists as p LEFT JOIN SongsTable AS s ON p.originfile=s.fullpathfilename ORDER BY id")
# Populate DB with playlists
playlistfolder = os.path.join(qluserfolder,"playlists")
playfiles = glob (os.path.join(playlistfolder,"*"))
processed_counter = 0
for f in playfiles:
linecounter = 0
print (f)
with open (f, 'r') as txt:
for line in txt:
linecounter += 1
processed_counter += 1
valuetuple = ( processed_counter,
f,
linecounter,
line[:-1]
)
con.execute ("INSERT INTO Playlists VALUES (?,?,?,?)", valuetuple)
con.commit ()
# Writting changes
AfectedPlaylists = con.execute ('SELECT playlistfile from Playlists_new where originfile <> targetpath GROUP BY playlistfile')
cursor = con.cursor ()
for Playlist in AfectedPlaylists:
cursor.execute ('SELECT originfile, targetpath FROM Playlists_new WHERE playlistfile=? ORDER BY id', Playlist)
if not dummy:
with open (Playlist[0], 'w') as txt:
for entry in cursor:
towrite = entry[1]
if towrite == None:
towrite = entry [0]
txt.write (towrite+"\n")
# renaming playlists
shutil.move(Playlist[0],Nextplaylistname(Playlist[0]))
#
###
### Removing empty folders
###
print ('\tRemoving empty folders.')
logging.info ('### Checking empty folders to delete them')
cursor = con.execute ('SELECT * FROM ScannedFolders')
for i in cursor:
dir_item = i[0]
logging.info (f'checking: {dir_item}')
if itemcheck(dir_item) != 'folder':
logging.warning ('\tDoes not exists or is not a folder. Skipping')
continue
while dir_item not in librarypaths :
if len (os.listdir (dir_item)) == 0:
if not dummy:
shutil.rmtree (dir_item)
logging.info ('\tDeleted (was empty)')
print (f"\t\tempty folder removed: {trimto (dir_item,40)} {dummymsg}")
logging.info (f'Empty folder removed: {dir_item}')
else:
break
dir_item = os.path.dirname (dir_item)
con.close ()
print ('Done!', 'Visit https://github.com/pablo33/quodlibet-filesorter for updates')