コード例 #1
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def config(self, set_option=None, get_option=None, value=None):
     logger.debug("%s (%s)\tconfig()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers))
     if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['admin']:
         logger.warn("%(user)s (%(ip)s) requested configuration data, but was denied because %(user)s is not a member of the admin group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)})
         raise cherrypy.HTTPError(401,'Not Authorized')
     if set_option:
         if not value:
             raise cherrypy.HTTPError(501,'No value provided for the requested option')
         if set_option not in cfg.keys():
             raise cherrypy.HTTPError(501,'The requested option does not exist')
         try:
             if type(cfg[set_option]) is types.ListType:
                 value = value.split(',')
             if type(cfg[set_option]) is types.BooleanType:
                 value = json.loads(value)
             if type(cfg[set_option]) is types.StringType:
                 value = str(value)
         except:
             raise cherrypy.HTTPError(501, 'The value provided was the wrong type. Expected a %s' % type(cfg[set_option]))
         try:
             cfg[set_option] = value
             cfg.save_config()
             cherrypy.response.headers['Content-Type'] = 'application/json'
             return json.dumps({'config': {set_option: cfg[set_option]}})
         except Exception as x:
             logger.error("Could not save configuration. The error was: %s" % str(x))
     if get_option:
         if get_option not in cfg.keys():
             raise cherrypy.HTTPError(501,'The requested option does not exist')
         cherrypy.response.headers['Content-Type'] = 'application/json'
         return json.dumps({'config': {get_option: cfg[get_option]}})
     cherrypy.response.headers['Content-Type'] = 'application/json'
     return json.dumps({'config': cfg})
コード例 #2
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def song(self, songid, bitrate=None):
     logger.debug("%s (%s)\tsong(songid=%s, bitrate=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songid, bitrate, cherrypy.request.headers))
     try:
         songid, format = songid.split('.')
     except:
         format = None
     return self.get_song(songid, format, bitrate)
コード例 #3
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def list_artists(self, query=None, output='json'):
     logger.debug("%s (%s)\tlist_artists(query=%s, output=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, query, output, cherrypy.request.headers))
     artists = self.library.artists(query)
     if output == 'json':
         cherrypy.response.headers['Content-Type'] = 'application/json'
         return json.dumps({'artists': artists})
     elif output == 'html':
         template = Template(file=os.path.join(cfg['THEME_DIR'], 'list_artists.tmpl'))
         template.artists = artists
         return template.respond()
     else:
         raise cherrypy.HTTPError(501,'Not Implemented')
コード例 #4
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def flush_db(self):
     logger.debug("%s (%s)\tshutdown()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers))
     if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['admin']:
         logger.warn("%(user)s (%(ip)s) requested that the database be flushed, but was denied because %(user)s is not a member of the admin group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)})
         raise cherrypy.HTTPError(401,'Not Authorized')
     try:
         cherrypy.response.headers['Content-Type'] = 'application/json'
         logger.info("Received flush database request, complying.")
         return json.dumps({'flush_db': True})
     except:
         pass
     finally:
         self.library.db.flush()
コード例 #5
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def shutdown(self):
     logger.debug("%s (%s)\tshutdown()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers))
     if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['admin']:
         logger.warn("%(user)s (%(ip)s) requested that the server shut down, but was denied because %(user)s is not a member of the admin group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)})
         raise cherrypy.HTTPError(401,'Not Authorized')
     try:
         cherrypy.response.headers['Content-Type'] = 'application/json'
         logger.info("Received shutdown request, complying.")
         self.transcoder.stop()
         self.library.scanner.stop()
         return json.dumps({'shutdown': True})
     except:
         pass
     finally:
         shutting_down.set()
         logger.debug("Stopping CherryPy.")
         cherrypy.engine.exit()
コード例 #6
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def get_tags(self, songid=None):
     logger.debug("%s (%s)\tget_tags(songid=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songid, cherrypy.request.headers))
     if not songid:
         raise cherrypy.HTTPError(501, "You must supply a song id.")
     if self.library.db.doc_exist(songid):
         song = self.library.db[songid]
         if song['type'] != 'song':
             raise cherrypy.HTTPError(501, "The specified document is not a song.")
         song['id'] = song['_id']
         del song['_id']
         del song['location']
         del song['_rev']
         del song['type']
         cherrypy.response.headers['Content-Type'] = 'application/json'
         return json.dumps({"song": song})
     else:
         raise cherrypy.HTTPError(404, "That song doesn't exist in the database.")
コード例 #7
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def list_songs(self, artists=None ,albums=None, start=None, length=None,
                query=None, list_all=False, archive=False, output='json'):
     logger.debug("%s (%s)\tlist_songs(artists=%s, albums=%s, start=%s, length=%s, query=%s, list_all=%s, archive=%s, output=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, artists, albums, start, length, query, list_all, archive, output, cherrypy.request.headers))
     if not list_all and not artists and not albums and not query and not archive:
         songs = []
     else:
         if artists:
             artists = artists.split(',')
         if albums:
             albums = albums.split(',')
         songs = self.library.songs(artists, albums, query)
     #log_message = "%s (%s) is " % (cherrypy.request.login, utils.find_originating_host(cherrypy.request.headers))
     #if query:
     #    log_message += 'searching for "%s".' % query
     #elif artists or albums:
     #    log_message += "browsing "
     #    if artists and not albums:
     #        log_message += "albums by %s." % ', '.join(self.library.artists(artists, query))
     #    elif albums:
     #        log_message += "%s by %s." % (', '.join(self.library.albums(artists, query)), ', '.join(self.library.artists(artists, albums, query)))
     #if not archive and not list_all:
     #    logger.info(log_message)
     if start and length:
         start = int(start)
         end = int(length) + start
         if len(songs) - 1 < end:
             end = -1
         songs = songs[start:end]
     songs.sort(key=itemgetter('albumartist', 'album', 'date', 'discnumber', 'tracknumber'))
     if output == 'json':
         cherrypy.response.headers['Content-Type'] = 'application/json'
         return json.dumps({'songs': songs})
     elif output == 'html':
         template = Template(file=os.path.join(cfg['THEME_DIR'], 'list_songs.tmpl'))
         template.songs = songs
         return template.respond()
     else:
         raise cherrypy.HTTPError(501,'Not Implemented')
コード例 #8
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def index(self):
     logger.debug("%s (%s)\tindex()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers))
     template = Template(file=os.path.join(cfg['THEME_DIR'], 'index.tmpl'))
     return template.respond()
コード例 #9
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def query(self, object_type, query=None, include=None, offset=0, limit=0, sort=None):
     logger.debug("%s (%s)\tquery(object_type=%s, query=%s, include=%s, offset=%s, limit=%s, sort=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, object_type, query, include, offset, limit, sort, cherrypy.request.headers))
     cherrypy.response.headers['Content-Type'] = 'application/json'
     if object_type not in ('artists', 'albums', 'songs'):
         raise cherrypy.HTTPError(501,'The requested option does not exist')
     retval = {'offset': offset, object_type: []}
     offset = int(offset)
     limit = int(limit)
     if query:
         query = shlex.split(query.lower())
     if include:
         include = include.lower().split(' ')
     if sort:
         sort = shlex.split(sort.lower())
     else:
         sort = ('albumartist', 'album', 'year', 'discnumber', 'tracknumber')
     tree = {}
     for item in self.library.cache.view('query/songs'):
         song = item['value']
         if query:
             match = True
             for term in query:
                 field = term.split(':', 1)
                 if len(field) == 1:
                     if term not in song['query']:
                         match = False
                         break
                 else:
                     field, word = field
                     if field not in song.keys():
                         match = False
                         break
                     else:
                         data = song[field]
                         if type(data) is not list:
                             data = [data]
                         if not data:
                             match = False
                             break
                         for entry in data:
                             if not fnmatch.fnmatch(unicode(entry).lower(), word):
                                 match = False
                                 break
             if not match:
                 continue
         try:
             del song['query']
         except:
             pass
         if object_type == 'songs':
             retval[object_type].append(song)
             continue
         if song.has_key('albumartist'):# and not (song['albumartist'].lower() in ('various', 'various artists', 'va')):
             artist = song['albumartist']
         else:
             artist = song['artist']
         if not tree.has_key(artist):
             tree[artist] = {}
         if not tree[artist].has_key(song['album']):
             tree[artist][song['album']] = []
         tree[artist][song['album']].append(song)
     if object_type in ('albums', 'artists'):
         artists = []
         for artist, albums in tree.iteritems():
             item = {'artist': artist}
             if (include and 'albums' in include) or object_type == 'albums':
                 item['albums'] = []
                 for album, songs in albums.iteritems():
                     album_item = {'album': album}
                     # fields = ('date', 'year', 'genre', 'totaltracks',
                     #           'tracktotal', 'totaldiscs', 'disctotal',
                     #           'compilation', 'albumartist')
                     fields = ('date', 'year')
                     for key in fields:
                         try:
                             album_item[key] = songs[0][key]
                         except:
                             continue
                     if include and ('songs' in include):
                         utils.complex_sort(album_item['songs'], *sort)
                     if album_item not in item['albums']:
                         item['albums'].append(album_item)
                 utils.complex_sort(item['albums'], *sort)
             elif include and ('songs' in include):
                 item['songs'] = []
                 for album, songs in albums.iteritems():
                     item['songs'] += songs
                 utils.complex_sort(item['songs'], *sort)
             artists.append(item)
         if object_type == 'albums':
             for artist in artists:
                 retval[object_type] += artist['albums']
             if include and ('artists' in include):
                 retval['artists'] = sorted(tree.keys())
         else:
             retval['artists'] = artists
     utils.complex_sort(retval[object_type], *sort)
     retval['total'] = len(retval[object_type])
     if limit:
         retval[object_type] = retval[object_type][offset:offset+limit]
     else:
         retval[object_type] = retval[object_type][offset:]
     return json.dumps(retval, indent=4)
コード例 #10
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def suggest(self, term=None):
     logger.debug("%s (%s)\tsuggest(term=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, term, cherrypy.request.headers))
     result = self.library.songs(suggest=True, query=term)
     return json.dumps(result)
コード例 #11
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def random(self, songs=None, artists=None ,albums=None, query=None, limit=None):
     logger.debug("%s (%s)\trandom(songs=%s, artists=%s, albums=%s, query=%s, limit=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songs, artists, albums, query, limit,  cherrypy.request.headers))
     song_list = []
     if artists:
         artists = artists.split(',')
     if albums:
         albums = albums.split(',')
     files = self.library.songs(artists, albums, query)
     if songs:
         songs = songs.split(',')
         for song in files:
             if song['id'] in songs:
                 songs_list.append(song)
     else:
         song_list = files
     shuffle(song_list)
     try:
         limit = int(limit)
     except:
         limit = len(song_list)
     cherrypy.response.headers['Content-Type'] = 'application/json'
     return json.dumps(song_list[:limit])
コード例 #12
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def update_library(self, ticket=None):
     #logger.debug("%s (%s)\tupdate()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers))
     if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['admin']:
         logger.warn("%(user)s (%(ip)s) requested a library update, but was denied because %(user)s is not a member of the admin group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)})
         raise cherrypy.HTTPError(401,'Not Authorized')
     cherrypy.response.headers['Content-Type'] = 'application/json'
     if not ticket:
         return json.dumps({"ticket": self.library.update()})
     else:
         return json.dumps(self.library.update(ticket=ticket))
コード例 #13
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def download(self, songs=None, artists=None ,albums=None, query=None):
     logger.debug("%s (%s)\tdownload(songs=%s, artists=%s, albums=%s, query=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songs, artists, albums, query, cherrypy.request.headers))
     if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['download']:
         logger.warn("%(user)s (%(ip)s) requested a download, but was denied because %(user)s is not a member of the download group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)})
         raise cherrypy.HTTPError(401,'Not Authorized')
     file_list = []
     if not songs and not artists and not albums and not query:
         raise cherrypy.HTTPError(501)
     elif songs:
         songs = songs.split(',')
         if len(songs) > 100:
             return "Too many songs! Please narrow your criteria."
         for song in songs:
             try:
                 file_list.append(self.library.db[song])
             except:
                 raise cherrypy.HTTPError(404,'Not Found')
     else:
         if artists:
             artists = artists.split(',')
         if albums:
             albums = albums.split(',')
         files = self.library.songs(artists, albums, query)
         if len(files) > 100:
             return "Too many songs! Please narrow your criteria."
         for song in files:
             file_list.append(self.library.db[song['id']])
     archive = create_archive(file_list)
     try:
         return serve_file(archive, 'application/zip', 'download.zip')
     except:
         logger.debug("Something went wrong while sending the archive.")
コード例 #14
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def get_cover(self, songid=None, size='original', download=False):
     logger.debug("%s (%s)\tget_cover(songid=%s, size=%s, download=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songid, size, download, cherrypy.request.headers))
     try:
         song = self.library.db[songid]
     except:
         raise cherrypy.HTTPError(404)
     try:
         size = int(size)
     except:
         size = 'original'
     cover = find_cover(song)
     if cover is None:
         raise cherrypy.HTTPError(404,'Not Found')
     if download:
         return serve_file(cover,
                     mimetypes.guess_type(cover)[0], "attachment", os.path.basename(cover))
     if size != 'original':
         artwork = resize_cover(song, cover, size)
     else:
         artwork = cover
     cherrypy.response.headers['Content-Type'] = mimetypes.guess_type(cover)[0]
     return serve_file(artwork,
                     mimetypes.guess_type(artwork)[0], "inline", os.path.basename(artwork))
コード例 #15
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
    def get_song(self, songid=None, format=False, bitrate=False):
        logger.debug("%s (%s)\tget_song(songid=%s, format=%s, bitrate=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songid, format, bitrate, cherrypy.request.headers))
        log_message = "%s (%s) is listening to " % (cherrypy.request.login, utils.find_originating_host(cherrypy.request.headers))
        last = self.multi_requests.get(songid, None)
        show_log = False
        if not last or (last and time.time() > (last + 30)):
            show_log = True
        self.multi_requests[songid] = time.time()
        try:
            range_request = cherrypy.request.headers['Range']
        except:
            range_request = "bytes=0-"
        try:
            song = self.library.db[songid]
            path = song['location']
        except:
            log_message += "a song ID which could not be found: %s" % str(songid)
            logger.error(log_message)
            raise cherrypy.HTTPError(404)
        log_message += '"%s" by %s from %s ' % (song['title'].encode(cfg['ENCODING']), song['artist'].encode(cfg['ENCODING']), song['album'].encode(cfg['ENCODING']))
        try:
            client_os, client_browser = httpagentparser.simple_detect(cherrypy.request.headers['User-Agent'])
        #    b = self.bc(cherrypy.request.headers['User-Agent'])
        #    if b:
        #        browser = "%s %s.%s on %s" % (b.name(), b.version()[0], b.version()[1], b.get("platform"))
        #    else:
        #        browser = cherrypy.request.headers['User-Agent']
        #    log_message += "using %s." % browser
        except:
            client_os = 'an OS'
            client_browser = 'a browser'
        try:
            if bitrate:
                bitrate = str(bitrate)
            force_transcode = False
            if bitrate and \
               (int(bitrate) in [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112,
                                          128, 160, 192, 224, 256, 320]) and \
               (song['bitrate'] / 1024 > int(bitrate)):
                force_transcode = True
        except:
            pass
        try:
            song_format = [song['mimetype'].split('/')[1],
                            os.path.splitext(path)[1].lower()[1:]]
        except:
            song_format = [os.path.splitext(path)[1].lower()[1:]]
        if True in [True for x in song_format if x in ['mp3']]:
            song_mime = 'audio/mpeg'
            song_format = ['mp3']
        elif True in [True for x in song_format if x in ['ogg', 'vorbis', 'oga']]:
            song_mime = 'audio/ogg'
            song_format = ['ogg', 'vorbis', 'oga']
        elif True in [True for x in song_format if x in ['m4a', 'aac', 'mp4']]:
            song_mime = 'audio/x-m4a'
            song_format = ['m4a', 'aac', 'mp4']
        else:
            song_mime = 'application/octet-stream'
        if not (format or bitrate):
            #log_message += " The client did not request any specific format or bitrate so the file is being sent as-is (%s kbps %s)." % (str(song['bitrate'] / 1000), str(song_format))
            log_message += "(%skbps %s)" % (str(song['bitrate']), song_format[0])
            if client_os and client_browser:
                log_message += " using %s on %s." % (client_browser, client_os)
            else:
                log_message += "."
            logger.info(log_message)
            if not os.name == 'nt':
                path = path.encode(cfg['ENCODING'])
            return serve_file(path, song_mime,
                                "inline", os.path.split(path)[1])
        if format:
            format = str(format).split(',')
        else:
            format = song_format
        logger.debug("The client wants %s and the file is %s" % (format, song_format))
        if True in [True for x in format if x in song_format] and not force_transcode:
            #if bitrate:
            #    log_message += " The client requested %s kbps %s, but the file is already %s kbps %s, so the file is being sent as-is." % (bitrate, format, str(song['bitrate'] / 1000), str(song_format))
            #else:
            #    log_message += " The client requested %s, but the file is already %s, so the file is being sent as-is." % (format, str(song_format))
            log_message += "(%skbps %s)" % (str(song['bitrate'] / 1000), song_format[0])
            if client_os and client_browser:
                log_message += " using %s on %s." % (client_browser, client_os)
            else:
                log_message += "."
            if show_log:
                logger.info(log_message)
            if not os.name == 'nt':
                path = path.encode(cfg['ENCODING'])
            return serve_file(path, song_mime,
                                "inline", os.path.split(path)[1])
        else:
            #if bitrate:
            #    log_message = " The client requested %s kbps %s, but the file is %s kbps %s, so we're transcoding the file for them." % (bitrate, format, str(song['bitrate'] / 1000), str(song_format))
            #else:
            #    log_message += " The client requested %s, but the file %s, so we're transcoding the file for them." % (format, str(song_format))
            log_message += "(transcoded from %skbps %s to %skbps %s)" % (str(song['bitrate'] / 1000), song_format[0], str(bitrate), format[0])
            if client_os and client_browser:
                log_message += " using %s on %s." % (client_browser, client_os)
            else:
                log_message += "."
            if show_log:
                logger.info(log_message)
        # If we're transcoding audio and the client is trying to make range
        # requests, we have to throw an error 416. This sucks because it breaks
        # <audio> in all the WebKit browsers I've tried, but at least it stops
        # them from spawning a zillion transcoder threads (I'm looking at you,
        # Chromium).
        if True in [True for x in format if x in ['mp3']]:
#            cherrypy.response.headers['Content-Length'] = '-1'
            if range_request != 'bytes=0-':
                logger.debug("Got a range request for a file that needs transcoded: %s" % range_request)
                raise cherrypy.HTTPError(416)
            else:
                cherrypy.response.headers['Content-Type'] = 'audio/mpeg'
                try:
                    if cherrypy.request.headers['Referer'].lower().endswith('jplayer.swf'):
                        cherrypy.response.headers['Content-Type'] = 'audio/mp3'
                except:
                    pass
                #cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
                return self.transcoder.transcode(path, 'mp3', bitrate)
        elif True in [True for x in format if x in ['ogg', 'vorbis', 'oga']]:
#            cherrypy.response.headers['Content-Length'] = '-1'
            if range_request != 'bytes=0-':
                logger.debug("Got a range request for a file that needs transcoded: %s" % range_request)
                raise cherrypy.HTTPError(416)
            else:
                cherrypy.response.headers['Content-Type'] = 'audio/ogg'
                #cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
                return self.transcoder.transcode(path, 'ogg', bitrate)
        elif True in [True for x in format if x in ['m4a', 'aac', 'mp4']]:
#            cherrypy.response.headers['Content-Length'] = '-1'
            if range_request != 'bytes=0-':
                logger.debug("Got a range request for a file that needs transcoded: %s" % range_request)
                raise cherrypy.HTTPError(416)
            else:
                cherrypy.response.headers['Content-Type'] = 'audio/x-m4a'
                #cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
                return self.transcoder.transcode(path, 'm4a', bitrate)
        else:
            raise cherrypy.HTTPError(501)
コード例 #16
0
ファイル: web.py プロジェクト: daveisadork/Blofeld
 def get_playlist(self, artists=None, albums=None, query=None, format=None,
                  list_all=False, bitrate=None, output='xspf'):
     logger.debug("%s (%s)\tget_playlist(artists=%s, albums=%s, query=%s, format=%s, list_all=%s, bitrate=%s, output=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, artists, albums, query, format, list_all, bitrate, output, cherrypy.request.headers))
     if not (list_all or artists or albums or query):
         songs = []
     else:
         if artists:
             artists = artists.split(',')
         if albums:
             albums = albums.split(',')
         songs = self.library.songs(artists, albums, query)
     playlist, ct = json_to_playlist(cherrypy.request.base, songs, output,
                                                             format, bitrate)
     cherrypy.response.headers['Content-Type'] = ct
     return playlist