21
21
https://github.com/JonnyJD/musicbrainz-isrcsubmit
24
__version__ = "2.0.0-beta.4"
24
__version__ = "2.0.0-beta.5"
25
25
AGENT_NAME = "isrcsubmit.py"
26
26
DEFAULT_SERVER = "musicbrainz.org"
27
27
# starting with highest priority
28
28
BACKENDS = ["mediatools", "media_info", "cdrdao", "libdiscid", "discisrc"]
29
BROWSERS = ["xdg-open", "x-www-browser", "iceweasel", "chromium", "opera"]
29
BROWSERS = ["xdg-open", "x-www-browser",
30
"firefox", "chromium", "chrome", "opera"]
31
# The webbrowser module is used when nothing is found in this list.
32
# This especially happens on Windows and Mac OS X (browser mostly not in PATH)
179
183
+ " disc. Possible backends are: %s." % ", ".join(BACKENDS)
180
184
+ " They are tried in this order otherwise." )
181
185
parser.add_option("--browser", metavar="BROWSER",
182
help="Program to open urls. If not chosen, we try these:\n"
183
+ ", ".join(BROWSERS))
186
help="Program to open URLs. This will be automatically detected"
187
" for most setups, if not chosen manually.")
184
188
parser.add_option("--force-submit", action="store_true", default=False,
185
189
help="Always open TOC/disc ID in browser.")
186
190
parser.add_option("--server", metavar="SERVER",
227
231
We want to know if the user has a "sane" which we can trust.
228
232
Unxutils has a broken 2.4 version. Which >= 2.16 should be fine.
230
devnull = open(os.devnull, "w")
232
# "which" should at least find itself (even without searching which.exe)
233
return_code = call(["which", "which"], stdout=devnull, stderr=devnull)
235
return False # no which at all
237
if (return_code == 0):
234
with open(os.devnull, "w") as devnull:
236
# "which" should at least find itself
237
return_code = call(["which", "which"],
238
stdout=devnull, stderr=devnull)
240
return False # no which at all
240
print('warning: your version of the tool "which" is buggy/outdated')
242
print(' unxutils is old/broken, GnuWin32 is good.')
242
if (return_code == 0):
245
print('warning: your version of the tool "which"'
246
' is buggy/outdated')
248
print(' unxutils is old/broken, GnuWin32 is good.')
245
251
def get_prog_version(prog):
246
252
if prog == "libdiscid":
260
266
if program == "libdiscid":
261
267
return "isrc" in discid.FEATURES
263
devnull = open(os.devnull, "w")
264
if options.sane_which:
265
p_which = Popen(["which", program], stdout=PIPE, stderr=devnull)
266
program_path = p_which.communicate()[0].strip()
267
if p_which.returncode == 0:
268
# check if it is only a symlink to another backend
269
real_program = os.path.basename(os.path.realpath(program_path))
270
if program != real_program and (
271
real_program in BACKENDS or real_program in BROWSERS):
273
print("WARNING: %s is a symlink to %s"
274
% (program, real_program))
277
return False # use real program instead, or higher priority
281
elif program in BACKENDS:
283
# we just try to start these non-interactive console apps
284
call([program], stdout=devnull, stderr=devnull)
269
with open(os.devnull, "w") as devnull:
270
if options.sane_which:
271
p_which = Popen(["which", program], stdout=PIPE, stderr=devnull)
272
program_path = p_which.communicate()[0].strip()
273
if p_which.returncode == 0:
274
# check if it is only a symlink to another backend
275
real_program = os.path.basename(os.path.realpath(program_path))
276
if program != real_program and (
277
real_program in BACKENDS or real_program in BROWSERS):
279
print("WARNING: %s is a symlink to %s"
280
% (program, real_program))
283
return False # use real program (target) instead
287
elif program in BACKENDS:
289
# we just try to start these non-interactive console apps
290
call([program], stdout=devnull, stderr=devnull)
292
298
def find_backend():
293
299
"""search for an available backend
312
318
if has_program(browser):
315
# default to the first "real" browser in the list, as of now: firefox
321
# This will use the webbrowser module to find a default
324
def open_browser(url, exit=False, submit=False):
325
"""open url in the selected browser, default if none
331
# silly but necessary for spaces in the path
332
os.execlp(options.browser, '"' + options.browser + '"', url)
334
# linux/unix works fine with spaces
335
os.execlp(options.browser, options.browser, url)
336
except OSError as err:
337
print_error("Couldn't open the url in %s: %s"
338
% (options.browser, str(err)))
340
print_error2("Please submit via:", url)
345
Popen([options.browser, url])
347
with open(os.devnull, "w") as devnull:
348
Popen([options.browser, url], stdout=devnull)
349
except FileNotFoundError as err:
350
print_error("Couldn't open the url in %s: %s"
351
% (options.browser, str(err)))
353
print_error2("Please submit via:", url)
359
# this supresses stdout
360
webbrowser.get().open(url)
361
except webbrowser.Error as err:
362
print_error("Couldn't open the url:", str(err))
364
print_error2("Please submit via:", url)
318
368
def get_real_mac_device(option_device):
319
369
"""drutil takes numbers as drives.
381
431
except AttributeError:
382
432
sys.stdout.write(msg)
384
def print_release_position(release, pos):
385
print_encoded("%d: %s - %s"
386
% (pos, release["artist-credit-phrase"], release["title"]))
387
if release.get("status"):
388
print("(%s)" % release["status"])
434
def print_release(release, position=None):
435
"""Print information about a release.
437
If the position is given, this should be an entry
438
in a list of releases (choice)
391
440
country = (release.get("country") or "").ljust(2)
392
441
date = (release.get("date") or "").ljust(10)
393
442
barcode = (release.get("barcode") or "").rjust(13)
399
448
catnumber_list.append(cat_number)
400
449
catnumbers = ", ".join(catnumber_list)
401
print_encoded("\t%s\t%s\t%s\t%s\n" % (country, date, barcode, catnumbers))
452
print_encoded("Artist:\t\t%s\n" % release["artist-credit-phrase"])
453
print_encoded("Release:\t%s" % release["title"])
455
print_encoded("%#2d:" % position)
456
print_encoded("%s - %s" % (
457
release["artist-credit-phrase"], release["title"]))
458
if release.get("status"):
459
print("(%s)" % release["status"])
463
print_encoded("Release Event:\t%s\t%s\n" % (date, country))
464
print_encoded("Barcode:\t%s\n" % release.get("barcode") or "")
465
print_encoded("Catalog No.:\t%s\n" % catnumbers)
466
print_encoded("MusicBrainz ID:\t%s\n" % release["id"])
468
print_encoded("\t%s\t%s\t%s\t%s\n" % (
469
country, date, barcode, catnumbers))
403
471
def print_error(*args):
404
472
string_args = tuple([str(arg) for arg in args])
416
484
% (options.backend, err.errno, err.strerror))
419
def ask_for_submission(url):
487
def ask_for_submission(url, print_url=False):
420
488
if options.force_submit:
421
489
submit_requested = True
424
492
submit_requested = user_input(" [y/N] ") == "y"
426
494
if submit_requested:
429
# silly but necessary for spaces in the path
430
os.execlp(options.browser, '"' + options.browser + '"', url)
432
# linux/unix works fine with spaces
433
os.execlp(options.browser, options.browser, url)
434
except OSError as err:
435
print_error("Couldn't open the url in %s: %s"
436
% (options.browser, str(err)))
437
print_error2("Please submit it via:", url)
495
open_browser(url, exit=True, submit=True)
440
497
print("Please submit the Disc ID with this url:")
444
500
class WebService2():
445
501
"""A web service wrapper that asks for a password when first needed.
460
516
if not self.auth:
462
518
if self.username is None:
463
printf("Please input your MusicBrainz username: ")
519
printf("Please input your MusicBrainz username (empty=abort): ")
464
520
self.username = user_input()
521
if len(self.username) == 0:
465
524
password = getpass.getpass(
466
525
"Please input your MusicBrainz password: ")
498
557
def submit_isrcs(self, tracks2isrcs):
499
558
if options.debug:
500
559
print("tracks2isrcs: %s" % tracks2isrcs)
503
musicbrainzngs.submit_isrcs(tracks2isrcs)
504
except AuthenticationError as err:
505
print_error("Invalid credentials: %s" % err)
507
except WebServiceError as err:
508
print_error("Couldn't send ISRCs: %s" % err)
511
print("Successfully submitted %d ISRCS." % len(tracks2isrcs))
563
musicbrainzngs.submit_isrcs(tracks2isrcs)
564
except AuthenticationError as err:
565
print_error("Invalid credentials: %s" % err)
569
except WebServiceError as err:
570
print_error("Couldn't send ISRCs: %s" % err)
573
print("Successfully submitted %d ISRCS." % len(tracks2isrcs))
537
600
self._release = None
538
601
self._backend = backend
539
602
self._verified = verified
603
self._asked_for_submission = False
540
604
self._common_includes=["artists", "labels", "recordings", "isrcs",
541
605
"artist-credits"] # the last one only for cleanup
542
606
self.read_disc() # sets self._disc
565
629
return url.replace("musicbrainz.org", options.server)
632
def asked_for_submission(self):
633
return self._asked_for_submission
568
636
def release(self):
569
637
"""The corresponding MusicBrainz release
571
639
This will ask the user to choose if the discID is ambiguous.
573
641
if self._release is None:
574
self._release = self.get_release(self._verified)
642
self.get_release(self._verified)
575
643
# can still be None
576
644
return self._release
604
672
selected_release = None
605
673
elif num_results > 1:
606
674
print("\nThis Disc ID is ambiguous:")
675
print(" 0: none of these\n")
676
self._asked_for_submission = True
607
677
for i in range(num_results):
608
678
release = results[i]
609
679
# printed list is 1..n, not 0..n-1 !
610
print_release_position(release, i + 1)
680
print_release(release, i + 1)
612
num = user_input("Which one do you want? [1-%d] "
682
num = user_input("Which one do you want? [0-%d] "
614
if int(num) not in range(1, num_results + 1):
684
if int(num) not in range(0, num_results + 1):
616
selected_release = results[int(num) - 1]
687
ask_for_submission(self.submission_url, print_url=True)
690
selected_release = results[int(num) - 1]
617
691
except (ValueError, IndexError):
618
692
print_error("Invalid Choice")
648
722
if chosen_release is None or options.force_submit:
650
724
url = self.submission_url
651
ask_for_submission(url) # submission will end the script
725
ask_for_submission(url, print_url=True)
653
728
print("recalculating to re-check..")
655
730
self.get_release(verified=True)
732
self._release = chosen_release
657
733
return chosen_release
687
763
# redundant to "libdiscid", but this might be handy for prerelease testing
688
764
elif backend == "discisrc":
690
br'Track\s+([0-9]+)\s+:\s+([A-Z]{2})-?([A-Z0-9]{3})-?(\d{2})-?(\d{5})'
766
r'Track\s+([0-9]+)\s+:\s+([A-Z]{2})-?([A-Z0-9]{3})-?(\d{2})-?(\d{5})'
692
768
if sys.platform == "darwin":
693
769
device = get_real_mac_device(device)
696
772
except OSError as err:
697
773
backend_error(err)
698
774
for line in isrcout:
775
line = decode(line) # explicitely decode from pipe
699
776
if options.debug:
700
777
printf(line) # already includes a newline
701
if line.startswith(b"Track") and len(line) > 12:
778
if line.startswith("Track") and len(line) > 12:
702
779
match = re.search(pattern, line)
703
780
if match is None:
704
781
print("can't find ISRC in: %s" % line)
706
783
track_number = int(match.group(1))
707
784
isrc = ("%s%s%s%s" % (match.group(2), match.group(3),
708
785
match.group(4), match.group(5)))
710
786
backend_output.append((track_number, isrc))
712
788
# media_info is a preview version of mediatools, both are for Windows
713
789
# this does some kind of raw read
714
790
elif backend in ["mediatools", "media_info"]:
716
br'ISRC\s+([0-9]+)\s+([A-Z]{2})-?([A-Z0-9]{3})-?(\d{2})-?(\d{5})'
792
r'ISRC\s+([0-9]+)\s+([A-Z]{2})-?([A-Z0-9]{3})-?(\d{2})-?(\d{5})'
717
793
if backend == "mediatools":
718
794
args = [backend, "drive", device, "isrc"]
724
800
except OSError as err:
725
801
backend_error(err)
726
802
for line in isrcout:
803
line = decode(line) # explicitely decode from pipe
727
804
if options.debug:
728
805
printf(line) # already includes a newline
729
if line.startswith(b"ISRC") and not line.startswith(b"ISRCS"):
806
if line.startswith("ISRC") and not line.startswith("ISRCS"):
730
807
match = re.search(pattern, line)
731
808
if match is None:
732
809
print("can't find ISRC in: %s" % line)
734
811
track_number = int(match.group(1))
735
812
isrc = ("%s%s%s%s" % (match.group(2), match.group(3),
736
813
match.group(4), match.group(5)))
738
814
backend_output.append((track_number, isrc))
740
816
# cdrdao will create a temp file and we delete it afterwards
895
972
url = "http://%s/isrc/%s" % (options.server, isrc)
896
973
if user_input("Open ISRC in the browser? [Y/n] ") != "n":
898
Popen([options.browser, url])
900
devnull = open(os.devnull, "w")
901
Popen([options.browser, url], stdout=devnull)
902
975
user_input("(press <return> when done with this ISRC) ")
914
987
disc = get_disc(options.device, options.backend)
915
988
disc.get_release()
917
print_encoded('Artist:\t\t%s\n' % disc.release["artist-credit-phrase"])
918
print_encoded('Release:\t%s\n' % disc.release["title"])
990
print_release(disc.release)
991
if not disc.asked_for_submission:
993
print("Is this information different for your release?")
994
ask_for_submission(disc.submission_url)
921
997
for medium in disc.release["medium-list"]: