forked from kelvinlawson/pykaraoke
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pykdb.py
1874 lines (1576 loc) · 73.5 KB
/
pykdb.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
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#******************************************************************************
#**** ****
#**** Copyright (C) 2010 Kelvin Lawson ([email protected]) ****
#**** Copyright (C) 2010 PyKaraoke Development Team ****
#**** ****
#**** This library is free software; you can redistribute it and/or ****
#**** modify it under the terms of the GNU Lesser General Public ****
#**** License as published by the Free Software Foundation; either ****
#**** version 2.1 of the License, or (at your option) any later version. ****
#**** ****
#**** This library is distributed in the hope that it will be useful, ****
#**** but WITHOUT ANY WARRANTY; without even the implied warranty of ****
#**** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ****
#**** Lesser General Public License for more details. ****
#**** ****
#**** You should have received a copy of the GNU Lesser General Public ****
#**** License along with this library; if not, write to the ****
#**** Free Software Foundation, Inc. ****
#**** 59 Temple Place, Suite 330 ****
#**** Boston, MA 02111-1307 USA ****
#******************************************************************************
""" This module provides support for the PyKaraoke song database, as
well as the user's settings file. """
import pygame
from pykconstants import *
from pykenv import env
import pykar, pycdg, pympg
import os, cPickle, zipfile, codecs, sys, time
import types
from cStringIO import StringIO
try:
from hashlib import md5
except ImportError:
from md5 import md5
# The amount of time to wait, in milliseconds, before yielding to the
# app for windowing updates during a long update process.
YIELD_INTERVAL = 1000
# The maximum number of zip files we will attempt to store in our zip
# file cache.
MAX_ZIP_FILES = 10
# Increment this version number whenever the settings version changes
# (which may not necessarily change with each PyKaraoke release).
# This will force users to re-enter their configuration information.
SETTINGS_VERSION = 6
# Increment this version number whenever the database version changes
# (which will also hopefully be infrequently).
DATABASE_VERSION = 2
class AppYielder:
""" This is a simple class that knows how to yield control to the
windowing system every once in a while. It is passed to functions
like SearchDatabase and BuildSearchDatabase--tasks which might
take a while to perform.
This class is just an abstract base class and does nothing. Apps
should subclass it and override Yield() to make any use from
it. """
def __init__(self):
self.lastYield = pygame.time.get_ticks()
def ConsiderYield(self):
now = pygame.time.get_ticks()
if now - self.lastYield >= YIELD_INTERVAL:
self.Yield()
self.lastYield = now
def Yield(self):
""" Override this method to actually yield control to the
windowing system. """
pass
class BusyCancelDialog:
"""This class implements a busy dialog to show a task is
progressing, and it includes a cancel button the user might click
on to interrupt the task. This is just an abstract base class and
does nothing. Apps should subclass from it. """
def __init__(self):
self.Clicked = False
def Show(self):
pass
def SetProgress(self, label, progress):
pass
def Destroy(self):
pass
class SongData:
"""This class is returned by SongStruct.GetSongDatas(), below. It
represents either a song file that exists on disk (and must still
be read), or it is a song file that was found in a zip archive
(and its data is available now)."""
def __init__(self, filename, data):
self.filename = filename
self.tempFilename = None
self.data = data
self.Ext = os.path.splitext(filename)[1].lower()
# By convention, if data is passed as None to the constructor,
# that means this file is a true file that exists on disk. On
# the other hand, if data is not None, then this is not a true
# file, and data contains its contents.
self.trueFile = (data == None)
def GetData(self):
"""Returns the actual data of the file. If the file has not
yet been read, this will read it and return the data."""
if self.data != None:
# The file has already been read; return that data.
return self.data
# The file has not yet been read
self.data = open(self.filename, 'rb').read()
return self.data
def GetFilepath(self):
"""Returns a full pathname to the file. If the file does not
exist on disk, this will write it to a temporary file and
return the name of that file."""
if self.trueFile:
# The file exists on disk already; just return its
# pathname.
return self.filename
if not self.tempFilename:
# The file does not exist on disk already; we have to write it
# to a temporary file.
prefix = globalSongDB.CreateTempFileNamePrefix()
basename = os.path.basename(self.filename)
# Add the tempfile prefix as well as the time to make the
# filename unique. This works around a bug
# in pygame.mixer.music on Windows which stops us deleting
# temp files until another song is loaded, preventing the
# same song from being played twice in a row.
self.tempFilename = prefix + str(time.time()) + basename
open(self.tempFilename, 'wb').write(self.data)
return self.tempFilename
# This functor is declared globally. It is assigned by
# SongDB.SelectSort(), so we can use bisect to search through
# the list also. This is an ugly hack around the fact that bisect has
# no facility to receive a key parameter, like sort does.
fileSortKey = None
class SongStruct:
""" This corresponds to a single song file entry, e.g. a .kar
file, or a .mp3/.cdg filename pair. The file might correspond to
a physical file on disk, or to a file within a zip file. """
# Type codes.
T_KAR = 0
T_CDG = 1
T_MPG = 2
def __init__(self, Filepath, settings,
Title = None, Artist = None, ZipStoredName = None, DatabaseAdd = False):
self.Filepath = Filepath # Full path to file or ZIP file
self.ZipStoredName = ZipStoredName # Filename stored in ZIP
# Assume there will be no title/artist info found
self.Title = Title or '' # (optional) Title for display in playlist
self.Artist = Artist or '' # (optional) Artist for display
self.Disc = '' # (optional) Disc for display
self.Track = -1 # (optional) Track for display
# Check to see if we are deriving song information from the filename
if settings.CdgDeriveSongInformation:
try:
self.Title = self.ParseTitle(Filepath, settings) # Title for display in playlist
self.Artist = self.ParseArtist(Filepath, settings) # Artist for display
self.Disc = self.ParseDisc(Filepath, settings) # Disc for display
self.Track = self.ParseTrack(Filepath, settings) # Track for display
except:
# Filename did not match requested scheme, set the title to the filepath
# so that the structure is still created, but without any additional info
#print "Filename format does not match requested scheme: %s" % Filepath
self.Title = os.path.basename(Filepath)
# If this SongStruct is being used to add to the database, and we are
# configured to exclude non-matching files, raise an exception. Otherwise
# allow it through. For database adds where we are not excluding such
# files the song will still be added to the database. For non-database
# adds we don't care anyway, we just want a SongStruct for passing around.
if DatabaseAdd and settings.ExcludeNonMatchingFilenames:
raise KeyError, "Excluding non-matching file: %s" % self.Title
# This is a list of other song files that share the same
# artist and title data.
self.sameSongs = []
# This is a pointer to the TitleStruct object that defined
# this song file, or None if it was not defined.
self.titles = None
# If the file ends in '.', assume we got it via tab-completion
# on a filename, and it really is meant to end in '.cdg'.
if self.Filepath != '' and self.Filepath[-1] == '.':
self.Filepath += 'cdg'
if ZipStoredName:
self.DisplayFilename = os.path.basename(ZipStoredName)
if isinstance(self.DisplayFilename, types.StringType):
self.DisplayFilename = self.DisplayFilename.decode(settings.ZipfileCoding)
else:
self.DisplayFilename = os.path.basename(Filepath)
if isinstance(self.DisplayFilename, types.StringType):
self.DisplayFilename = self.DisplayFilename.decode(settings.FilesystemCoding)
# Check the file type based on extension.
self.Type = None
ext = os.path.splitext(self.DisplayFilename)[1].lower()
if ext in settings.KarExtensions:
self.Type = self.T_KAR
elif ext in settings.CdgExtensions:
self.Type = self.T_CDG
elif ext in settings.MpgExtensions:
self.Type = self.T_MPG
if ext == '.mpg' or ext == '.mpeg':
self.MpgType = 'mpg'
else:
self.MpgType = ext[1:]
def ParseTitle(self, filepath, settings):
""" Parses the file path and returns the title of the song. If the filepath cannot be parsed a KeyError exception is thrown. If the settings contains a file naming scheme that we do not support a KeyError exception is thrown."""
title = ''
# Make sure we are to parse information
if settings.CdgDeriveSongInformation:
if settings.CdgFileNameType == 0: # Disc-Track-Artist-Title.Ext
# Make sure we can parse the filepath
if len(filepath.split("-")) == 4:
title = filepath.split("-")[3] # Find the Title in the filename
else:
raise KeyError, "Invalid type for file: %s!" % filepath
elif settings.CdgFileNameType == 1: # DiscTrack-Artist-Title.Ext
# Make sure we can parse the filepath
if len(filepath.split("-")) == 3:
title = filepath.split("-")[2] # Find the Title in the filename
else:
raise KeyError, "Invalid type for file: %s!" % filepath
elif settings.CdgFileNameType == 2: # Disc-Artist-Title.Ext
# Make sure we can parse the filepath
if len(filepath.split("-")) == 3:
title = filepath.split("-")[2] # Find the Title in the filename
else:
raise KeyError, "Invalid type for file: %s!" % filepath
elif settings.CdgFileNameType == 3: # Artist-Title.Ext
# Make sure we can parse the filepath
if len(filepath.split("-")) == 2:
title = filepath.split("-")[1] # Find the Title in the filename
else:
raise KeyError, "Invalid type for file: %s!" % filepath
else:
raise KeyError, "File name type is invalid!"
# Remove the first and last space
title = title.strip(" ")
# Remove the filename extension
title = os.path.splitext(title)[0]
#print "Title parsed: %s" % title
return title
def ParseArtist(self, filepath, settings):
""" Parses the filepath and returns the artist of the song. """
artist = ''
# Make sure we are to parse information
if settings.CdgDeriveSongInformation:
if settings.CdgFileNameType == 0: # Disc-Track-Artist-Title.Ext
artist = filepath.split("-")[2] # Find the Artist in the filename
elif settings.CdgFileNameType == 1: # DiscTrack-Artist-Title.Ext
artist = filepath.split("-")[1] # Find the Artist in the filename
elif settings.CdgFileNameType == 2: # Disc-Artist-Title.Ext
artist = filepath.split("-")[1] # Find the Artist in the filename
elif settings.CdgFileNameType == 3: # Artist-Title.Ext
artist = filepath.split("-")[0] # Find the Artist in the filename
artist = os.path.basename(artist)
else:
raise KeyError, "File name type is invalid!"
# Remove the first and last space
artist = artist.strip(" ")
#print "Artist parsed: %s" % artist
return artist
def ParseDisc(self, filepath, settings):
""" Parses the filepath and returns the disc name of the song. """
disc = ''
# Make sure we are to parse information
if settings.CdgDeriveSongInformation:
if settings.CdgFileNameType == 0: # Disc-Track-Artist-Title.Ext
disc = filepath.split("-")[0] # Find the Disc in the filename
elif settings.CdgFileNameType == 1: # DiscTrack-Artist-Title.Ext
disc = filepath.mid(0, filepath.length - 2) # Find the Disc in the filename
elif settings.CdgFileNameType == 2: # Disc-Artist-Title.Ext
disc = filepath.split("-")[0] # Find the Disc in the filename
elif settings.CdgFileNameType == 3: # Artist-Title.Ext
disc = ''
else:
raise KeyError, "File name type is invalid!"
# Remove the first and last space
disc = disc.strip(" ")
# Remove the filename path
disc = os.path.basename(disc)
#print "Disc parsed: %s" % disc
return disc
def ParseTrack(self, filepath, settings):
""" Parses the file path and returns the track for the song. """
track = ''
# Make sure we are to parse information
if settings.CdgDeriveSongInformation:
if settings.CdgFileNameType == 0: # Disc-Track-Artist-Title.Ext
track = filepath.split("-")[1] # Find the Track in the filename
elif settings.CdgFileNameType == 1: # DiscTrack-Artist-Title.Ext
track = filepath.mid(filepath.length - 2, 2) # Find the Track in the filename
elif settings.CdgFileNameType == 2: # Disc-Artist-Title.Ext
track = ''
elif settings.CdgFileNameType == 3: # Artist-Title.Ext
track = ''
else:
raise KeyError, "File name type is invalid!"
# Remove the first and last space
#track = track.strip(" ")
#print "Track parsed: %s" % track
return track
def MakeSortKey(self, str):
""" Returns a suitable key to use for sorting, by lowercasing
and removing articles from the indicated string. """
str = str.strip().lower()
if str:
# Remove a leading parenthetical phrase.
if str[0] == '(':
rparen = str.index(')')
if rparen != ')':
str = str[rparen + 1:].strip()
if str:
# Remove a leading article.
firstWord = str.split()[0]
if firstWord in ['a', 'an', 'the']:
str = str[len(firstWord):].strip()
return str
def MakePlayer(self, songDb, errorNotifyCallback, doneCallback):
"""Creates and returns a player of the appropriate type to
play this file, if possible; or returns None if the file
cannot be played (in which case, the errorNotifyCallback will
have already been called with the error message). """
settings = songDb.Settings
constructor = None
if self.Type == self.T_CDG:
constructor = pycdg.cdgPlayer
elif self.Type == self.T_KAR:
constructor = pykar.midPlayer
elif self.Type == self.T_MPG:
if self.MpgType == 'mpg' and settings.MpgNative and pympg.movie:
# Mpg files can be played internally.
constructor = pympg.mpgPlayer
else:
# Other kinds of movies require an external player.
constructor = pympg.externalPlayer
else:
ext = os.path.splitext(self.DisplayFilename)[1]
errorNotifyCallback("Unsupported file format " + ext)
return None
# Try to open the song file.
try:
player = constructor(self, songDb, errorNotifyCallback,
doneCallback)
except:
errorNotifyCallback("Error opening file.\n%s\n%s" % (sys.exc_info()[0], sys.exc_info()[1]))
return None
return player
def GetSongDatas(self):
"""Returns a list of SongData objects; see SongData.
Usually there is only one element in the list: the file named
by this SongStruct. In the case of .cdg files, however, there
may be more tuples; the first tuple will be the file named by
this SongStruct, and the remaining tuples will correspond to
other files with the same basenames but different extensions
(so that the .mp3 or .ogg associated with a cdg file may be
recovered)."""
songDatas = []
if not self.Filepath:
return songDatas
if not os.path.exists(self.Filepath):
error = 'No such file: %s' % (self.Filepath)
raise ValueError(error)
dir = os.path.dirname(self.Filepath)
if dir == "":
dir = "."
root, ext = os.path.splitext(self.Filepath)
prefix = os.path.basename(root + ".")
if self.ZipStoredName:
# It's in a ZIP file; unpack it.
zip = globalSongDB.GetZipFile(self.Filepath)
filelist = [self.ZipStoredName]
root, ext = os.path.splitext(self.ZipStoredName)
prefix = os.path.basename(root + ".")
if self.Type == self.T_CDG:
# In addition to the .cdg file, we also have to get
# out the mp3/ogg/whatever audio file that comes with
# the .cdg file. Just extract out any files that have
# the same basename.
for name in zip.namelist():
if name != self.ZipStoredName and name.startswith(prefix):
filelist.append(name)
# We'll continue looking for matching files outside the
# zip, too.
for file in filelist:
try:
data = zip.read(file)
songDatas.append(SongData(file, data))
except:
print "Error in ZIP containing " + file
else:
# A non-zipped file; this is an easy case.
songDatas.append(SongData(self.Filepath, None))
if self.Type == self.T_CDG:
# In addition to the .cdg file, we also have to find the
# mp3/ogg/whatever audio file that comes with the .cdg
# file, just as above, when we were pulling them out of
# the zip file. This time we are just looking for loose
# files on the disk.
for file in os.listdir(dir):
# Handle potential byte-strings with invalid characters
# that startswith() will not handle.
try:
file = unicode(file)
except UnicodeDecodeError:
file = file.decode("ascii", "replace")
try:
prefix = unicode(prefix)
except UnicodeDecodeError:
prefix = prefix.decode("ascii", "replace")
# Check for a file which matches the prefix
if file.startswith(prefix):
path = os.path.join(dir, file)
if path != self.Filepath:
songDatas.append(SongData(path, None))
# Now we've found all the matching files.
return songDatas
def getTextColour(self, selected):
""" Returns a suitable colour to use when rendering the text
of this song line in the pykaraoke_mini song index. """
if selected:
fg = (255, 255, 255)
else:
# Determine the color of the text.
fg = (180, 180, 180)
if self.Type == self.T_KAR:
# Midi file: color it red.
fg = (180, 72, 72)
elif self.Type == self.T_CDG:
# CDG+MP3: color it blue.
fg = (72, 72, 180)
elif self.Type == self.T_MPG:
# MPEG file: color it yellow.
fg = (180, 180, 72)
return fg
def getBackgroundColour(self, selected):
""" Returns a suitable colour to use when rendering the
background of this song line in the pykaraoke_mini song
index. """
if not selected:
bg = (0, 0, 0)
else:
if self.Type == self.T_KAR:
# Midi file: color it red.
bg = (120, 0, 0)
elif self.Type == self.T_CDG:
# CDG+MP3: color it blue.
bg = (0, 0, 120)
elif self.Type == self.T_MPG:
# MPEG file: color it yellow.
bg = (120, 120, 0)
return bg
def getDisplayFilenames(self):
""" Returns the list of all of the filenames that share the
same artist/title with this song file. The list is formatted
as a single comma-delimited string. """
if self.sameSongs:
return ', '.join(map(lambda f: f.DisplayFilename, self.sameSongs))
return self.DisplayFilename
def getTypeSort(self):
"""Defines a sorting order by type, for sorting the sameSongs
list. """
# We negate self.Type, so that the sort order is: mpg, cdg,
# kar. This means that MPG files have priority over CDG which
# have priority over KAR, for the purposes of coloring the
# files in the mini index.
return (-self.Type, self.DisplayFilename)
def getMarkKey(self):
""" Returns a key for indexing into markedSongs, for uniquely
identifying this particular song file. """
return (self.Filepath, self.ZipStoredName)
def __cmp__(self, other):
"""Define a sorting order between SongStruct objects. This is
used in bisect, to quickly search for a SongStruct in a sorted
list. It relies on fileSortKey (above) having being filled in
already. """
global fileSortKey
a = fileSortKey(self)
b = fileSortKey(other)
if a == b:
return cmp(id(self), id(other))
if a < b:
return -1
return 1
class TitleStruct:
""" This represents a single titles.txt file. Its filename is
preserved so it can be rewritten later, to modify a title and/or
artist associated with a song. """
def __init__(self, Filepath, ZipStoredName = None):
self.Filepath = Filepath # Full path to file or ZIP file
self.ZipStoredName = ZipStoredName # Filename stored in ZIP
self.songs = []
# This is false unless the titles file has been locally
# modified and needs to be flushed.
self.dirty = False
def read(self, songDb):
""" Reads the titles.txt file, and stores the results in the
indicated db. This is intended to be called during db
scan. """
if self.ZipStoredName != None:
zip = songDb.GetZipFile(self.Filepath)
unzipped_data = zip.read(self.ZipStoredName)
sfile = StringIO(unzipped_data)
self.__readTitles(songDb, sfile,
os.path.join(self.Filepath, self.ZipStoredName))
else:
self.__readTitles(songDb, None, self.Filepath)
def rewrite(self, songDb):
""" Rewrites the titles.txt file with the current data. """
if self.ZipStoredName != None:
sfile = StringIO()
self.__writeTitles(songDb, sfile,
os.path.join(self.Filepath, self.ZipStoredName))
unzipped_data = sfile.getvalue()
songDb.DropZipFile(self.Filepath)
zip = zipfile.ZipFile(self.Filepath, 'a', zipfile.ZIP_DEFLATED)
# Since the lame Python zipfile.py implementation won't
# replace an existing file, we have to rename it out of
# the way.
self.__renameZipElement(zip, self.ZipStoredName)
zip.writestr(self.ZipStoredName, unzipped_data)
zip.close()
else:
self.__writeTitles(songDb, None, self.Filepath)
def __renameZipElement(self, zip, name1, name2 = None):
""" Renames the file within the archive named "name1" to
"name2". To avoid major rewriting of the archive, it is
required that len(name1) == len(name2).
If name2 is omitted or None, a new, unique name is
generated based on the old name.
"""
zinfo = zip.getinfo(name1)
zip._writecheck(zinfo)
if name2 is None:
# Replace the last letters with digits.
i = 0
n = str(i)
name2 = name1[:-len(n)] + n
while name2 in zip.NameToInfo:
i += 1
n = str(i)
name2 = name1[:-len(n)] + n
if len(name1) != len(name2):
raise RuntimeError, \
"Cannot change length of name with rename()."
filepos = zip.fp.tell()
zip.fp.seek(zinfo.header_offset + 30, 0)
zip.fp.write(name2)
zinfo.filename = name2
zip.fp.seek(filepos, 0)
def __readTitles(self, songDb, catalogFile, catalogPathname):
self.songs = []
dirname = os.path.split(catalogPathname)[0]
if catalogFile == None:
# Open the file for reading.
try:
catalogFile = open(catalogPathname, "rU")
except:
print "Could not open titles file %s" % (repr(catalogPathname))
return
for line in catalogFile:
try:
line = line.decode('utf-8').strip()
except UnicodeDecodeError:
line = line.decode('utf-8', 'replace')
print "Invalid characters in %s:\n%s" % (repr(catalogPathname), line)
if line:
tuple = line.split('\t')
if len(tuple) == 2:
filename, title = tuple
artist = ''
elif len(tuple) == 3:
filename, title, artist = tuple
else:
print "Invalid line in %s:\n%s" % (repr(catalogPathname), line)
continue
# Allow a forward slash in the file to stand in for
# whatever the OS's path separator is.
filename = filename.replace('/', os.path.sep)
pathname = os.path.join(dirname, filename)
song = songDb.filesByFullpath.get(pathname, None)
if song is None:
print "Unknown file in %s:\n%s" % (repr(catalogPathname), repr(filename))
else:
song.titles = self
self.songs.append(song)
song.Title = title.strip()
song.Artist = artist.strip()
if song.Title:
songDb.GotTitles = True
if song.Artist:
songDb.GotArtists = True
def __makeRelTo(self, filename, relTo):
""" Returns the filename expressed as a relative path to
relTo. Both file paths should be full paths; relTo should
already have had normcase and normpath applied to it, and
should end with a slash. """
filename = os.path.normpath(filename)
norm = os.path.normcase(filename)
prefix = os.path.commonprefix((norm, relTo))
# The common prefix must end with a slash.
slash = prefix.rfind(os.sep)
if slash != -1:
prefix = prefix[:slash + 1]
filename = filename[len(prefix):]
relTo = relTo[len(prefix):]
numSlashes = relTo.count(os.sep)
if numSlashes > 1:
backup = '..' + os.sep
filename = backup * (numSlashes - 1) + filename
return filename
def __writeTitles(self, songDb, catalogFile, catalogPathname):
dirname = os.path.split(catalogPathname)[0]
if catalogFile == None:
# Open the file for writing.
try:
catalogFile = open(catalogPathname, "w")
except:
print "Could not rewrite titles file %s" % (repr(catalogPathname))
return
relTo = os.path.normcase(os.path.normpath(catalogPathname))
if relTo[-1] != os.sep:
relTo += os.sep
for song in self.songs:
filename = song.Filepath
if song.ZipStoredName:
filename = os.path.join(filename, song.ZipStoredName)
filename = self.__makeRelTo(filename, relTo)
# Use forward slashes instead of the native separator, to
# make a more platform-independent titles.txt file.
filename = filename.replace(os.sep, '/')
line = filename
if songDb.GotTitles or songDb.GotArtists:
line += '\t' + song.Title
if songDb.GotArtists:
line += '\t' + song.Artist
line = line.encode('utf-8')
catalogFile.write(line + '\n')
class FontData:
""" This stores the font description selected by the user.
Hopefully it is enough information to be used both in wx and in
pygame to reference a unique font on the system. """
def __init__(self, name = None, size = None, bold = False, italic = False):
# name may be either a system font name (if size != None) or a
# filename (if size == None).
self.name = name
self.size = size
self.bold = bold
self.italic = italic
def __repr__(self):
if not self.size:
return "FontData(%s)" % (repr(self.name))
else:
return "FontData(%s, %s, %s, %s)" % (
repr(self.name), repr(self.size), repr(self.bold), repr(self.italic))
def getDescription(self):
desc = self.name
if self.size:
desc += ',%spt' % (self.size)
if self.bold:
desc += ',bold'
if self.italic:
desc += ',italic'
return desc
# SettingsStruct used as storage only for settings. The instance
# can be pickled to save all user's settings.
class SettingsStruct:
# This is the list of the encoding strings we offer the user to
# select from. You can also type your own.
Encodings = [
'cp1252',
'iso-8859-1',
'iso-8859-2',
'iso-8859-5',
'iso-8859-7',
'utf-8',
]
# This is the set of CDG zoom modes.
Zoom = [
'quick', 'int', 'full', 'soft', 'none',
]
ZoomDesc = {
'quick' : 'a pixelly scale, maintaining aspect ratio',
'int' : 'like quick, reducing artifacts a little',
'full' : 'like quick, but stretches to fill the entire window',
'soft' : 'a high-quality scale, but may be slow on some hardware',
'none' : 'keep the display in its original size',
}
# Some audio cards seem to support only a limited set of sample
# rates. Here are the suggested offerings.
SampleRates = [
48000,
44100,
22050,
11025,
5512,
]
# A list of possible file name deriving combinations.
# The combination order is stored and used in the parsing algorithm.
# Track is assumed to be exactly 2 digits.
FileNameCombinations = [
'Disc-Track-Artist-Title',
'DiscTrack-Artist-Title',
'Disc-Artist-Title',
'Artist-Title'
]
def __init__(self):
self.Version = SETTINGS_VERSION
# Set the default settings, in case none are stored on disk
self.FolderList = []
self.CdgExtensions = [ '.cdg' ]
self.KarExtensions = [ '.kar', '.mid' ]
self.MpgExtensions = [ '.mpg', '.mpeg', '.avi' ]
self.IgnoredExtensions = []
self.LookInsideZips = True
self.ReadTitlesTxt = True
self.CheckHashes = False
self.DeleteIdentical = False
if env == ENV_WINDOWS:
self.FilesystemCoding = 'cp1252'
else:
self.FilesystemCoding = 'iso-8859-1'
self.ZipfileCoding = 'cp1252'
self.WindowSize = (640, 480) # Size of the window for PyKaraoke
self.FullScreen = False # Determines if the karaoke player should be full screen
self.NoFrame = False # Determies if the karaoke player should have a window frame.
# SDL specific parameters; some settings may work better on
# certain hardware than others
self.DoubleBuf = True
self.HardwareSurface = True
self.PlayerSize = (640, 480) # Size of the karaoke player
self.PlayerPosition = None # Initial position of the karaoke player
self.SplitVertically = True
self.AutoPlayList = True # Enables or disables the auto play on the play-list
self.DoubleClickPlayList = True # Enables or disables the double click for playing from the play-list
self.ClearFromPlayList = True # Enables or disables clearing the playlist with a right click on the play list
self.Kamikaze = False # Enables or disables the kamikaze button
self.UsePerformerName = False # Enables or disables the prompting for a performers name.
self.PlayFromSearchList = True # Enables or disables the playing of a song from the search list
self.DisplayArtistTitleCols = False # Enables or disables display of artist/title columns
self.SampleRate = 44100
self.NumChannels = 2
self.BufferMs = 50
self.UseMp3Settings = True
# This value is a time in milliseconds that will be used to
# shift the time of the lyrics display relative to the video.
# It is adjusted by the user pressing the left and right
# arrows during singing, and is persistent during a session.
# Positive values make the lyrics anticipate the music,
# negative values delay them.
self.SyncDelayMs = 0
# KAR/MID options
self.KarEncoding = 'cp1252' # Default text encoding in karaoke files
self.KarFont = FontData("DejaVuSans.ttf")
self.KarBackgroundColour = (0, 0, 0)
self.KarReadyColour = (255,50,50)
self.KarSweepColour = (255,255,255)
self.KarInfoColour = (0, 0, 200)
self.KarTitleColour = (100, 100, 255)
self.MIDISampleRate = 44100
# CDG options
self.CdgZoom = 'int'
self.CdgUseC = True
self.CdgDeriveSongInformation = False # Determines if we should parse file names for song information
self.CdgFileNameType = -1 # The style index we are using for the file name parsing
self.ExcludeNonMatchingFilenames = False # Exclude songs from database if can't derive song info
# MPEG options
self.MpgNative = True
self.MpgExternalThreaded = True
self.MpgExternal = 'mplayer -fs "%(file)s"'
if env == ENV_WINDOWS:
self.MpgExternal = '"C:\\Program Files\\Windows Media Player\\wmplayer.exe" "%(file)s" /play /close /fullscreen'
elif env == ENV_GP2X:
self.FullScreen = True
self.PlayerSize = (320, 240)
self.CdgZoom = 'none'
# Reduce the default sample rate on the GP2x to save time.
self.MIDISampleRate = 11025
self.MpgExternal = './mplayer_cmdline "%(file)s"'
self.MpgExternalThreaded = False
self.BufferMs = 250
# Define the CPU speed for various activities. We're
# conservative here and avoid overclocking by default.
# The user can push these values higher if he knows his
# GP2X can handle it.
self.CPUSpeed_startup = 240
self.CPUSpeed_wait = 33
self.CPUSpeed_menu_idle = 33
self.CPUSpeed_menu_slow = 100
self.CPUSpeed_menu_fast = 240
self.CPUSpeed_load = 240
self.CPUSpeed_cdg = 200
self.CPUSpeed_kar = 240
self.CPUSpeed_mpg = 200
# This is a trivial class used to wrap the song database with a
# version number.
class DBStruct:
def __init__(self):
self.Version = DATABASE_VERSION
pass
# Song database class with methods for building the database, searching etc
class SongDB:
def __init__(self):
# Filepaths and titles are stored in a list of SongStruct instances
self.FullSongList = []
# This is the same list, with songs of the same artist/title
# removed.
self.UniqueSongList = []
# Here's those lists again, cached into various different
# sorts.
self.SortedLists = {}
# And this is just the currently-active song list, according
# to selected sort.
self.SongList = []
# The list of TitlesFiles we have found in our scan.
self.TitlesFiles = []
# A cache of zip files.
self.ZipFiles = []
# Set true if there are local changes to the database that
# need to be saved to disk.
self.databaseDirty = False
# Some databases may omit either or both of titles and
# artists, relying on filenames instead.
self.GotTitles = False
self.GotArtists = False
# Create a SettingsStruct instance for storing settings
# in case none are stored.
self.Settings = SettingsStruct()
# All temporary files use this prefix
self.TempFilePrefix = "00Pykar__"
self.SaveDir = self.getSaveDirectory()
self.TempDir = self.getTempDirectory()
self.CleanupTempFiles()
def getSaveDirectory(self):
""" Returns the directory in which the settings files should
be saved. """
# If we have PYKARAOKE_DIR defined, use it.
dir = os.getenv('PYKARAOKE_DIR')
if dir:
return dir
if env == ENV_GP2X:
# On the GP2X, just save db files in the root directory.
# Makes it easier to find them, and avoids directory
# clutter.
return '.'