#!/usr/bin/python import urllib, string, getopt, os, time, sys, tempfile, re, mimetools, socket Version = "1.48" # # beam-back.py # # Copyright GNU GENERAL PUBLIC LICENSE Version 2 # http://www.gnu.org/copyleft/gpl.html # # Homepage: http://beam-back.sourceforge.net # Author : Kal # # Acknowledgements # ================ # # SourceForge crew -hosting this project # my.mp3.com -providing such a great service # Robert A. Seace -m3u playlist idea and code # Tim Carroll -infinite streaming suggestions # Jake Kauth -filename cleaning, logging class and rename code # Jiva Devoe -pls patch for direct http server handling # Nathan Shafer -mp3 filename fix and logging tweak patch # Florian Bomers -added -y and -q flags, fix for .m3u playlists # # def Usage(): print print "Usage: beam-back.py [options] m3u_file" print print " -c clobber files instead of appending -# " print " -d dir top directory to create files/subdirs in " print " -f enable framing and discard non-mp3 data " print " -i use ICY title data from a shoutcast stream " print " (this option is much improved) " print " -l log output to beam-back.log file " print " -m create a local .m3u playlist (in top dir) " print " -n 1 name as \"artist - title.mp3\" (default) " print " -n 2 name as \"title.mp3\" " print " -n 3 name as \"title.mp3\" in subdir \"artist\" " print " -n 4 name as \"artist - title.mp3\" in subdir \"artist\" " print " -n #u same as -n 1 to 4 except sub underscores for spaces " print " -p persistent re-open pls until keyboard interrupt" print " or size/duration limit reached" print " -s file name for temporary files (otherwise auto generated) " print " -t msec trim msec milliseconds off the end of ICY files " print " -u file name for unfinished streams (unfinished-stream.mp3)" print " -v verbose mode " print " -q quiet mode (otherwise progress printed)" print " -z size download size limit in megabytes" print " -y sec download duration limit in seconds" print print "Set your netscape application for m3u suffixes to something like:" print "xterm -e beam-back.py -v -n 3 %s" print sys.exit() class FilenameFactory: # # generates filenames that won't overwrite an # an existing file if clobber == 0 by appending a -# # def __init__(self, clobber = 0): self.clobber = clobber self.cleaner = re.compile(r'[\000\\/:*?"<>|()]') def set_clobber(self, clobber): self.clobber = clobber def clean(self, text): return self.cleaner.sub('', text) def spacer(self, text): textlist = string.split(text) return string.joinfields(textlist, "_") def make(self, filename): newfilename = filename if self.clobber == 0: dirname = os.path.dirname(newfilename) basename = os.path.basename(newfilename) (root, ext) = os.path.splitext(basename) copycnt = 0 while os.path.isfile(newfilename): copycnt = copycnt + 1 newfilename = dirname + '/' + root + "-" + `copycnt` + ext return newfilename def makeindir(self, dirname, filename): basename = self.clean(filename) newfilename = dirname + "/" + basename if self.clobber == 0: (root, ext) = os.path.splitext(basename) copycnt = 0 while os.path.isfile(newfilename): copycnt = copycnt + 1 newfilename = dirname + '/' + root + "-" + `copycnt` + ext return newfilename class Logger: # # open a new log file in the target dir if logging # a race condition if there are tons of beam-back scripts # starting at the same time and should really use locking # but that would be overkill for this app # def __init__(self, logname = "beam-back.log", verbose = 0, logging = 0): self.logname = logname self.verbose = verbose self.logging = logging self.prefix = "beam-back: " if self.logging: self.logfp = open(self.logname, "w") self.logfp.write("beam-back v" + Version + "\n") self.logfp.write(self.prefix + "logging to " + self.logname + "\n") self.logfp.close() print (self.prefix + "logging to "+self.logname) def logit(self, logline): if self.verbose: print self.prefix + logline if self.logging: self.logfp = open(self.logname, "a") self.logfp.write(self.prefix + logline + "\n") self.logfp.close() class ICY: # # ICY protocol class # def __init__(self, host = '', port = 80): self.metaint = 0 self.myport = port self.mydir = "/" # strip leading and trailing whitespace host = string.lstrip(host) host = string.rstrip(host) # strip leading http:// if any if string.lower(host[:4]) == "http": self.myhost = host[7:] else: self.myhost = host # strip trailing / if any if self.myhost[-1] == '/': self.myhost = self.myhost[:-1] # strip out dir if any p1 = string.find(self.myhost, "/") if p1 != -1: self.mydir = self.myhost[p1:] self.myhost = self.myhost[:p1] # handle port number in myhost, if any p1 = string.find(self.myhost, ':') if p1 != -1: portstr = self.myhost[p1+1:] self.myhost = self.myhost[:p1] try: self.myport = string.atoi(portstr) except: raise socket.error, "bad port in URL" def icyopen(self, icydata = 0): # # try to connect and request icy meta data if icydata == 1 # self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.myhost, self.myport)) self.sock.send("GET " + self.mydir + " HTTP/1.0\r\n") if icydata == 1: self.sock.send("Icy-Metadata: 1\r\n") self.sock.send('\r\n') # convert the socket to a file handle self.file = self.sock.makefile('rb') # grab the statusline self.statusline = self.file.readline() try: statuslist = string.split(self.statusline) except ValueError: # assume it is okay since so many servers are badly configured statuslist = ["ICY", "200"] if statuslist[1] == "302": # moved temporarily status, look for location header while 1: line = self.file.readline() if not line: return -1 if string.find(line, "Location") == 0: self.location = line[10:] # strip leading and trailing whitespace self.location = string.lstrip(self.location) self.location = string.rstrip(self.location) return -2 if statuslist[1] != "200": return -1 # grab any headers for a max of 10 lines icyline = "" linecnt = 0 while linecnt < 10: icyline = self.file.readline() linecnt = linecnt + 1 # break on short line (ie. really should be a blank line) if len(icyline) < 4: break # strip leading and trailing whitespace icyline = string.lstrip(icyline) icyline = string.rstrip(icyline) # strip out icy headers if string.find(icyline, "icy-notice1") != -1: self.icynotice1 = icyline if string.find(icyline, "icy-notice2") != -1: self.icynotice2 = icyline if string.find(icyline, "icy-name") != -1: self.icyname = icyline if string.find(icyline, "icy-genre") != -1: self.icygenre = icyline if string.find(icyline, "icy-url") != -1: self.icyurl = icyline if string.find(icyline, "icy-pub") != -1: self.icypub = icyline if string.find(icyline, "icy-br") != -1: self.icybr = icyline self.bitrate = string.atoi(self.icybr[7:]) if string.find(icyline, "icy-metaint") != -1: self.icymetaint = icyline self.metaint = string.atoi(self.icymetaint[12:]) return self.file def getmetaint(self): return self.metaint def getbitrate(self): return self.bitrate def getstatusline(self): return self.statusline def getlocation(self): return self.location def icyclose(self): if self.file: self.file.close() self.file = None if self.sock: self.sock.close() self.sock = None class MPEG: # # routines to analyze MPEG frame headers # def __init__(self): self.mpeg = "" self.layer = "" self.bitrate = 0 self.samplerate = 0 self.genretable = ["Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Unknown"] def getmpeg(self): return self.mpeg def getlayer(self): return self.layer def getbitrate(self): return self.bitrate def getsamplerate(self): return self.samplerate # # calculate the mp3 frame length given a mp3 header # returns None or the frame length # def FrameLen(self, header): # get the 4 bytes of the header into integers h1 = ord(header[0]) h2 = ord(header[1]) h3 = ord(header[2]) h4 = ord(header[3]) # MPEG Version ID vID = (h2 & 24) >> 3 # 0 = V2.5, 1 = reserved, 2 = v2, 3 = v1 if vID == 0: vName = "2.5" elif vID == 2: vName = "2" elif vID == 3: vName = "1" else: return None self.mpeg = vName # MPEG Layer layerID = (h2 & 6) >> 1 # 1=layerIII, 2=layerII, 3=layerI if layerID == 1: layerName = "III" elif layerID == 2: layerName = "II" elif layerID == 3: layerName = "I" else: return None self.layer = layerName # determine the bitrate rateID = (h3 & 240) >> 4 if vName == "1" and layerName == "III": ratetable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1] elif vName == "1" and layerName == "II": ratetable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1] elif vName == "1" and layerName == "I": ratetable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1] elif vName == "2" and layerName == "III": ratetable = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1] elif vName == "2" and layerName == "II": ratetable = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1] elif vName == "2" and layerName == "I": ratetable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1] elif vName == "2.5" and layerName == "III": ratetable = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1] elif vName == "2.5" and layerName == "II": ratetable = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1] elif vName == "2.5" and layerName == "I": ratetable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1] else: ratetable = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] br = ratetable[rateID] * 1000 self.bitrate = br if br <= 0: return None # determine the sr sampleID = (h3 & 12) >> 2 if vName == "1": sampletable = [44100, 48000, 32000, -1] elif vName == "2": sampletable = [22050, 24000, 16000, -1] elif vName == "2.5": sampletable = [11025, 12000, 8000, -1] else: sampletable = [-1, -1, -1, -1] sr = sampletable[sampleID] self.samplerate = sr if sr <= 0: return None # grab the padding size in bytes padbit = (h3 & 2) >> 1 if padbit == 1 and layerName == "III": padsize = 1 elif padbit == 1 and layerName == "II": padsize = 1 elif padbit == 1 and layerName == "I": padsize = 4 else: padsize = 0 # calculate frame length if vName == "1": if layerName == "I": framelen = ((48 * br) / sr) + padbit else: framelen = ((144 * br) / sr) + padbit else: if layerName == "I": framelen = ((24 * br) / sr) + padbit else: framelen = ((72 * br) / sr) + padbit #if layerName == "I": # framelen = (12 * br / sr + padsize) * 4 #else: # framelen = 144 * br / sr + padsize # channel mode channel = (h4 & 192) >> 6 return framelen # # find the mp3 frame start index given a chunk of data # by checking for sync at the start of several headers # returns None or the frame start index # def FrameStart(self, data): f1 = -1 dlen = len(data) - 100 # find a possible frame sync while 1: f1 = f1 + 1 if f1 >= dlen: break if ord(data[f1]) == 255 and ord(data[f1+1]) >= 224: flen1 = self.FrameLen(data[f1:f1+4]) if flen1 == None: continue if f1 + flen1 >= dlen: return None if ord(data[f1+flen1]) == 255 and ord(data[f1+flen1+1]) >= 224: flen2 = self.FrameLen(data[f1 + flen1:f1 + flen1 + 4]) if flen2 == None: continue if f1 + flen1 + flen2 >= dlen: return None if ord(data[f1 + flen1 + flen2]) == 255 and ord(data[f1 + flen1 + flen2+1]) >= 224: break else: continue else: continue if f1 >= dlen: return None else: return f1 # # parse the ID3 V1 tag given the 128 bytes starting with tag # returns None or a tuple with the id3 info # def ID3v1(self, data): if data[0:3] == "TAG": title = string.rstrip(data[3:33]) artist = string.rstrip(data[33:63]) album = string.rstrip(data[63:93]) year = string.rstrip(data[93:97]) comment = string.rstrip(data[97:127]) genre = string.rstrip(data[127:128]) gid = ord(genre) if gid <= 80: return [title, artist, album, year, comment, self.genretable[gid]] else: return [title, artist, album, year, comment, `gid`] else: return None # # parse the ID3 V2 tag given mp3 data # returns None or a tuple with the id3 info # INCOMPLETE # def ID3v2len(self, data): if data[0:3] == "ID3" and ord(data[3]) == 3: # get the 4 bytes of the header for frame size s1 = ord(data[5]) & 127 s2 = ord(data[6]) & 127 s3 = ord(data[7]) & 127 s4 = ord(data[8]) & 127 size = (s1 << 21) + (s2 << 14) + (s3 << 7) + (s4) + 10 print size return None elif data[0:3] == "ID3" and ord(data[3]) == 4: # NOT FINISHED # get the 4 bytes of the header for frame size s1 = ord(data[6]) & 127 s2 = ord(data[7]) & 127 s3 = ord(data[8]) & 127 s4 = ord(data[9]) & 127 size = (s1 << 21) + (s2 << 14) + (s3 << 7) + (s4) print size + 10 return None else: return None if __name__=="__main__": # # default options # opt_verbose = 0 opt_quiet = 0 opt_logging = 0 opt_topdir = "." opt_naming = "1" opt_playlist = 0 opt_clobber = 0 opt_icydata = 0 opt_persistent = 0 opt_unfinished = "unfinished-stream.mp3" opt_size = 0 opt_duration = 0 opt_trim = 0 opt_framing = 0 opt_tempfiles = "" # # parse the command line options # if len(sys.argv) == 1: Usage() try: opts, args = getopt.getopt(sys.argv[1:], "cd:ilmn:pu:vz:s:t:fy:q") except getopt.error, reason: print reason sys.exit(-1) if len(args) == 0: Usage() # # check clobber, verbose/quiet, logging and topdir options first # for opt in opts: (lopt, ropt) = opt if lopt == "-c": opt_clobber = 1 elif lopt == "-l": opt_logging = 1 elif lopt == "-v": opt_verbose = 1 elif lopt == "-q": opt_quiet = 1 elif lopt == "-d": if os.path.isdir(ropt): opt_topdir = ropt else: print "bad top directory" sys.exit() # # check the os we are running on and print the version # if not opt_quiet: if sys.platform == "win32": print "beam-back v" + Version + " win32" else: print "beam-back v" + Version + " linux" # fix the top dir name to not end in slash if opt_topdir[-1:] == "/": opt_topdir = opt_topdir [:-1] # create the filename factory object fnfactory = FilenameFactory(opt_clobber) # create the logger object logname = fnfactory.makeindir(opt_topdir, "beam-back.log") logger = Logger(logname, opt_verbose, opt_logging) # log the options set before log object created if opt_clobber == 1: logger.logit("clobber option set") if opt_logging == 1: logger.logit("logging option set") if opt_verbose == 1: logger.logit("verbose option set") logger.logit("storing files in " + opt_topdir) # # handle other options and log as needed # for opt in opts: (lopt, ropt) = opt if lopt == "-i": opt_icydata = 1 logger.logit("icydata option set") elif lopt == "-m": # opt_playlist = 1 means create a new playlist # opt_playlist = 2 means playlist created add mp3 file name opt_playlist = 1 logger.logit("playlist option set") elif lopt == "-n": if ropt == "1" or ropt == "2" or ropt == "3" or ropt == "4" or ropt == "1u" or ropt == "2u" or ropt == "3u" or ropt == "4u": opt_naming = ropt logger.logit("naming option set to " + ropt) else: print "bad naming option" sys.exit() elif lopt == "-p": opt_persistent = 1 logger.logit("persistent option set") elif lopt == "-u": opt_unfinished = ropt logger.logit("unfinished option set to " + ropt) elif lopt == "-z": try: opt_size = string.atoi(ropt) logger.logit("size limit option set to " + ropt + "M") except: opt_size = 0 logger.logit("bad size limit option") elif lopt == "-y": try: opt_duration = string.atoi(ropt) logger.logit("duration limit option set to " + ropt + " seconds") except: opt_duration = 0 logger.logit("bad duration limit option") elif lopt == "-s": opt_tempfiles = ropt logger.logit("tempfiles option set to " + ropt) elif lopt == "-t": try: opt_trim = string.atoi(ropt) logger.logit("trim option set to " + ropt) except: opt_trim = 0 logger.logit("bad trim option") elif lopt == "-f": opt_framing = 1 logger.logit("framing option set") inputfile = args[0] # name of the m3u file taggedcnt = 0 # number of mp3 files downloaded notagcnt = 0 # number of files that look bad (no ID3 tags) failedcnt = 0 # number of files that failed the transfer plname = "" # the playlist name # # get the input file # lines = [] if(inputfile[:7] == "http://"): if inputfile[-4:] == ".pls": tmpfile = "/tmp/beam-back.pls" else: tmpfile = "/tmp/beam-back.m3u" fTmp = urllib.urlopen(inputfile) fTmpWrite = open(tmpfile, 'w') plsLines = fTmp.readlines() for line in plsLines: fTmpWrite.write(line) inputfile = tmpfile fTmpWrite.close() logline = "inputfile " + inputfile logger.logit(logline) inputfp = open(inputfile, "r") while (1): line = inputfp.readline() if not line: break if string.find(line, "#") != 0: lines.append(line) inputfp.close() urls = [] opt_plsfile = 0 if inputfile[-4:] == ".pls": newurl = "" for plsfile in lines: if string.find(plsfile, "File") == 0: p1 = string.find(plsfile, "=") newurl = plsfile[p1+1:] # strip leading and trailing whitespace newurl = string.lstrip(newurl) newurl = string.rstrip(newurl) if opt_verbose: logline = "server url " + newurl logger.logit(logline) urls.append(newurl) opt_plsfile = 1 if not newurl: logline = "playlist handling file error" logger.logit(logline) sys.exit() else: # m3u file urls = lines if opt_verbose: for newurl in urls: logline = "server url " + newurl logger.logit(logline) # now go thru the urls # for url in urls: # pause before starting song to give my.mp3.com # a chance at authorizing the url before we access it time.sleep(1) # strip leading and trailing whitespace url = string.lstrip(url) url = string.rstrip(url) # if the url consists only of a host treat it like # a playlist (ie. icecast m3u playlists) p1 = string.rfind(url, "/") if p1 == -1 or p1 > len(url) - 5 or opt_plsfile == 1: opt_plsfile = 1 if url[-1] == "/": url = url[:-1] logline = "Trying stream " + url logger.logit(logline) else: logline = "Getting file " + url[p1+1:] logger.logit(logline) # # Open the URL using urllib for m3u and icy for pls # bytesread = 0 kilobytes = 0 blocksize = 4*1024 if opt_plsfile: if opt_verbose: logline = "ICY " + url[7:] logger.logit(logline) icy = ICY(url[7:]) try: urlfp = icy.icyopen(opt_icydata) if urlfp == -2: logline = "\"" + url + "\" moved to " + icy.getlocation() logger.logit(logline) url = icy.getlocation() logline = "Trying new location " + url logger.logit(logline) icy = ICY(url) urlfp = icy.icyopen(opt_icydata) if urlfp < 0: logline = icy.getstatusline() logger.logit(logline) continue else: urlfp = urllib.urlopen(url) except IOError: logline = "Caught IOError on " + url + "." logger.logit(logline) continue else: urlfp = urllib.urlopen(url) # # create a temp file for the data # tempfile.tempdir = opt_topdir tmpfilename = tempfile.mktemp(".mp3") if opt_tempfiles != "": tmpfilename = fnfactory.makeindir(opt_topdir, opt_tempfiles) if opt_verbose: logline = "Writing to tempfile " + tmpfilename logger.logit(logline) tmpfp = open(tmpfilename, "wb") mpeg = MPEG() icyfilename = "" icycount = 1 icychunk = "" try: blockread = urlfp.read(blocksize) # discard the beginning junk that doesn't look like a mp3 frame #if opt_framing == 1 and opt_plsfile == 0: if opt_framing == 1: fstart = mpeg.FrameStart(blockread) floop = 0 while fstart == None and floop < 7: if opt_verbose: logger.logit("NO Frame " + `fstart` + " MPEG v" + mpeg.getmpeg() + " Layer " + mpeg.getlayer()) moreread = urlfp.read(blocksize) blockread = blockread + moreread fstart = mpeg.FrameStart(blockread) floop = floop + 1 if fstart != None: if opt_verbose: logger.logit("Frame " + `fstart` + " MPEG v" + mpeg.getmpeg() + " Layer " + mpeg.getlayer()) blockread = blockread[fstart:] else: logger.logit("Could not find the frame start. Framing disabled.") opt_framing = 0 # if a duration limit is set, remember the start time if opt_duration > 0: startClock = time.time() # would prefer time.clock(), but that doesn't work on AMD?? while 1: if len(blockread) == 0 and opt_persistent == 0: break bytesread = bytesread + len(blockread) if bytesread > 100000: # track bytesread with two variables kilobytes = kilobytes + 100 bytesread = bytesread - 102400 mesgbytes = kilobytes + (bytesread / 1000) if not opt_quiet: print "Data read: " + `mesgbytes` + "K CTRL-C to stop\r", sys.stdout.flush() if opt_icydata == 0: # write the block, non icy data if len(blockread) > 0: tmpfp.write(blockread) else: # we really want to compute the following expression # (bytesread + (kilobytes * 1024)) <= icycount * icy.getmetaint(): # but this will overflow after 2G of capturing (~30hrs) # # so we use lhs to icyread and rhs to icynext # and reduce the counters icyread = 0 icynext = 0 if (bytesread + (kilobytes * 1024)) <= icycount * icy.getmetaint(): # save a block that doesn't need icy processing icychunk = icychunk + blockread else: # handle a block with ICY metadata icycount = icycount + 1 # write a warning if the beginning looks messed up if ord(blockread[0]) > 20: logger.logit("VERY Suspicious beginning of meta data. ") # determine the length via the length byte metalen = ord(blockread[0]) * 16 # read some more data to maintain chunk boundaries if metalen == 0: metadata = blockread[:1] moreread = urlfp.read(1) blockread = blockread[1:] + moreread # write the icy chunk previous to this block tmpfp.write(icychunk) # save this chunk for writing icychunk = blockread else: if blockread[metalen] != "\0": logger.logit("VERY Suspicious ending of meta data. ") metadata = blockread[:metalen] moreread = urlfp.read(metalen + 1) blockread = blockread[metalen+1:] + moreread # analyze the metadata for the stream title # before writing the block p1 = string.find(metadata, "StreamTitle") if p1 == -1: # couldn't find the StreamTitle heading logger.logit("Couldn't find StreamTitle in metadata. ") # write the icy chunk previous to this block tmpfp.write(icychunk) # save this block for writing later as a chunk icychunk = blockread else: p2 = string.find(metadata, "'", p1); p3 = string.find(metadata, "';", p2+1) mp3name = icyfilename if p2+1 == p3: icyfilename = "notitle.mp3" else: icyfilename = metadata[p2+1:p3] + ".mp3" logline = "%-40s" % icyfilename logger.logit(logline) if opt_icydata == 1: # first stream title encountered # set the flag to use this title as name of file # at the end of the song opt_icydata = 2 # write the icy chunk previous to this block tmpfp.write(icychunk) # save this block for writing later as a chunk icychunk = blockread elif opt_icydata == 2: # close the file and rename it correctly tmpfp.close() newmp3name = fnfactory.makeindir(opt_topdir, mp3name) # trim the end if needed if opt_trim > 0: trimsize = opt_trim * icy.getbitrate() tmpfp = open(tmpfilename, "rb") tmpdata = tmpfp.read() tmpfp.close() tmpfp = open(newmp3name, "wb") if len(tmpdata) <= trimsize: tmpfp.write(tmpdata) else: # find a frame to trim at fstart = mpeg.FrameStart(tmpdata[:-trimsize]) if fstart == None: tmpfp.write(tmpdata[:-trimsize]) else: tmpfp.write(tmpdata[:-(trimsize - fstart)]) tmpfp.close() os.unlink(tmpfilename) else: os.rename(tmpfilename, newmp3name) # open a new file tempfile.tempdir = opt_topdir tmpfilename = tempfile.mktemp(".mp3") tmpfp = open(tmpfilename, "wb") # write out the icychunk to the new file tmpfp.write(icychunk) # save this block for writing later as a chunk icychunk = blockread # read a new block for processing blockread = urlfp.read(blocksize) # check the size and duration limit, only compare kilo vs mega duration_reached = (opt_duration > 0) and ((time.time() - startClock) > opt_duration) size_reached = (opt_size > 0) and ((kilobytes * 1024) > opt_size) if size_reached or duration_reached: urlfp.close() tmpfp.close() newmp3name = "" if opt_icydata == 2: newmp3name = fnfactory.makeindir(opt_topdir, icyfilename) else: newmp3name = fnfactory.makeindir(opt_topdir, opt_unfinished) os.rename(tmpfilename, newmp3name) if not opt_quiet: # write a newline print "" if size_reached: logline = "size limit reached saved to " + newmp3name else: logline = "duration limit reached saved to " + newmp3name logger.logit(logline) sys.exit() # possibly reopen if persistent if len(blockread) == 0 and opt_persistent == 1: # close and reopen if opt_plsfile: logline = "Reopening PLS " + url[7:] logger.logit(logline) icy.icyclose() urlfp = icy.icyopen(opt_icydata) else: logline = "Reopening M3U " + url logger.logit(logline) urlfp.close() urlfp = urllib.urlopen(url) urlfp.close() tmpfp.close() except KeyboardInterrupt: # rename the temp file to the interrupted name urlfp.close() tmpfp.close() if not opt_quiet: # write a newline print "" if opt_icydata == 2: newmp3name = fnfactory.makeindir(opt_topdir, icyfilename) os.rename(tmpfilename, newmp3name) logline = "keyboard interrupt, saved to " + newmp3name else: newmp3name = fnfactory.makeindir(opt_topdir, opt_unfinished) os.rename(tmpfilename, newmp3name) logline = "keyboard interrupt, saved to " + newmp3name logger.logit(logline) sys.exit() # # non-icy data streams from here on # # # need to check if the size of returned data is suspiciously small # if bytesread < 2048 and kilobytes <= 0: datafp = open(tmpfilename, "rb") data = datafp.read() datafp.close() logline = "FAILED \"" + url + "\" " logger.logit(logline) failedcnt = failedcnt + 1 continue # grab the id3 info tmpfp = open(tmpfilename, "rb") tmpfp.seek(-128, 2) id3 = tmpfp.read(128) tmpfp.close() if id3[0:3] == "TAG": taggedcnt = taggedcnt + 1 title = string.rstrip(id3[3:33]) artist = string.rstrip(id3[33:63]) album = string.rstrip(id3[63:93]) else: notagcnt = notagcnt + 1 title = "notitle" artist = "noartist" album = "noalbum" # clean up id3 strings before using them for filename creation title = fnfactory.clean(title) artist = fnfactory.clean(artist) album = fnfactory.clean(album) # fix up the spaces to underscores if specified divider = " - " if len(opt_naming) > 1: if opt_naming[1] == "u": title = fnfactory.spacer(title) artist = fnfactory.spacer(artist) album = fnfactory.spacer(album) divider = "-" # compute a playlist name if necessary if opt_playlist == 1: plcnt = 1 if not album: plname = artist + ".m3u" else: plname = artist + divider + album + ".m3u" newplname = fnfactory.makeindir(opt_topdir, plname) playfp = open(newplname, "w") playfp.close() opt_playlist = 2 # # compute the output mp3 filename # mp3name = "" mp3dir = "" if opt_naming[0] == "1": mp3dir = opt_topdir mp3name = artist + divider + title + ".mp3" elif opt_naming[0] == "2": mp3dir = opt_topdir mp3name = title + ".mp3" elif opt_naming[0] == "3": mp3dir = opt_topdir + "/" + artist if not os.path.isdir(mp3dir): os.mkdir(mp3dir) mp3name = title + ".mp3" elif opt_naming[0] == "4": mp3dir = opt_topdir + "/" + artist if not os.path.isdir(mp3dir): os.mkdir(mp3dir) mp3name = artist + divider + title + ".mp3" else: # should never be here but just in case have a default mp3dir = opt_topdir mp3name = artist + divider + title + ".mp3" # if the mp3name already exists create a copy count name newmp3name = fnfactory.makeindir(mp3dir, mp3name) os.rename(tmpfilename, newmp3name) if bytesread < 2048 and kilobytes <= 0: logline = "SUSPICIOUSLY SMALL " + newmp3name logger.logit(logline) else: logline = "creating " + newmp3name logger.logit(logline) # write the playlist if opt_playlist == 2: playfp = open(plname, "a") playfp.write(newmp3name + "\n") playfp.close() # show the summary logline = "==========" logger.logit(logline) logline = "success on " + `taggedcnt` + " file(s)." logger.logit(logline) if (notagcnt > 0): logline = "no ID3 tags on " + `notagcnt` + " file(s)." logger.logit(logline) if (failedcnt > 0): logline = "failed transfers on " + `failedcnt` + " file(s)." logger.logit(logline) if not opt_quiet: print "Done." if (failedcnt > 0) or (notagcnt > 0): time.sleep(10) else: time.sleep(5)