def download_from_youtube(url: str) -> str: with wrap_context('downloading from youtube', url=url): log.info('downloading from youtube...', url=url) uid = str(uuid.uuid4()) filename = f'trimmer_dl_{uid}' ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl': f'{filename}.%(ext)s' } with youtube_dl.YoutubeDL(ydl_opts) as ydl: retcode = ydl.download([url]) assert retcode == 0 full_filename = f'{filename}.mp3' assert os.path.isfile(full_filename) log.info('song downloaded', tmpfile=full_filename) return full_filename
def respond_to_client(self, response: HttpResponse): with wrap_context('responding to client'): self.send_response_only(response.status_code) if has_header(response.headers, 'Content-Encoding'): del response.headers['Content-Encoding'] if self.config.verbose >= 2: log.debug('removing Content-Encoding header') if not has_header(response.headers, 'Content-Length') and \ not has_header(response.headers, 'Transfer-Encoding') and response.content: response.headers['Content-Length'] = str(len(response.content)) log.warn('adding missing Content-Length header') if has_header(response.headers, 'Content-Length') and has_header( response.headers, 'Transfer-Encoding'): del response.headers['Content-Length'] log.warn( 'removed Content-Length header conflicting with Transfer-Encoding' ) for name, value in response.headers.items(): self.send_header(name, value) self.end_headers() if self.config.allow_chunking and response.headers.get( 'Transfer-Encoding') == 'chunked': send_chunked_response(self.wfile, response.content) else: self.wfile.write(response.content) self.close_connection = True if self.config.verbose >= 2: log.debug('> response sent', client_addr=self.client_address[0], client_port=self.client_address[1])
def generate_response(self, request_0: HttpRequest) -> HttpResponse: with wrap_context('generating response'): request = request_0.transform(self.extensions.transform_request) if request != request_0 and self.config.verbose >= 2: log.debug('request transformed') immediate_reponse = self.find_immediate_response(request) if immediate_reponse: return immediate_reponse.log('> immediate response', self.config.verbose) self.cache.clear_old() if self.cache.has_cached_response(request): return self.cache.replay_response(request).log( '> Cache: returning cached response', self.config.verbose) if self.config.replay and self.config.verbose: log.warn('request not found in cache', path=request.path) response: HttpResponse = proxy_request( request, default_url=self.config.dst_url, timeout=self.config.timeout, verbose=self.config.verbose) if self.cache.saving_enabled(request, response): self.cache.save_response(request, response) return response
def proxy_request(request: HttpRequest, default_url: str, timeout: int, verbose: int) -> HttpResponse: dst_url = request.dst_url if request.dst_url else default_url with logerr(): with wrap_context('proxying to URL', dst_url=dst_url, path=request.path, content=request.content): url = f'{dst_url}{request.path}' if verbose: log.debug(f'>> proxying to', url=url) response = requests.request(request.method, url, verify=False, allow_redirects=False, stream=False, timeout=timeout, headers=request.headers, data=request.content) content: bytes = response.content http_response = HttpResponse(status_code=response.status_code, headers=dict(response.headers), content=content) return http_response.log('<< received', verbose) # Bad Gateway response error_msg = f'Proxying failed: {dst_url}' return HttpResponse(status_code=502, headers={ 'X-Man-Error': 'proxying failed', }).set_content(error_msg)
def read_mp3_artist_title(mp3_file: str) -> Tuple[str, str]: with wrap_context('extracting mp3 metadata', mp3_file=mp3_file): tag_artist, tag_title = read_mp3_tags(mp3_file) file_artist, file_title = extract_filename_artist_title(mp3_file) artist = tag_artist or file_artist title = tag_title or file_title log.info('metadata inferred', artist=artist, title=title) return artist, title
def tag_mp3(mp3_file: str, artist: str, title: str): with wrap_context('tagging mp3', artist=artist, title=title, mp3_file=mp3_file): log.info('tagging mp3...', artist=artist, title=title) audiofile = eyed3.load(mp3_file) audiofile.tag.artist = artist audiofile.tag.title = title audiofile.tag.save(version=ID3_V1_1) audiofile.tag.save(version=ID3_V2_4)
def read_mp3_tags(mp3_file: str) -> Tuple[str, str]: with wrap_context('reading mp3 tags'): audiofile = eyed3.load(mp3_file) if audiofile is None or audiofile.tag is None: log.warn('no IDv3 tags read', mp3_file=mp3_file) return '', '' artist = _oremptystr(audiofile.tag.artist).strip() title = _oremptystr(audiofile.tag.title).strip() log.info('IDv3 tags read', mp3_file=mp3_file, artist=artist, title=title) return artist, title
def normalize_song(mp3_file: str, no_trim: bool, no_fade: bool, no_normalize: bool, user_trim_start: Optional[float] = None, user_trim_end: Optional[float] = None, user_gain: Optional[float] = None): with wrap_context('normalizing mp3', mp3_file=mp3_file): log.info('loading song...', mp3_file=mp3_file) song = AudioSegment.from_mp3(mp3_file) if not no_normalize: if user_gain is not None: gain = user_gain else: volume = calculate_volume(song) log.info('normalizing volume level...', volume=f'{volume:.2f}dB', dBFS=f'{song.dBFS:.2f}dB') gain = -volume song = song.apply_gain(gain) log.info('volume normalized', gain=f'{gain:.2f}dB') if not no_trim: log.info('trimming silence...') start_trim = user_trim_start * 1000 if user_trim_start is not None else detect_leading_silence( song) end_trim = user_trim_end * 1000 if user_trim_end is not None else detect_leading_silence( song.reverse(), margin=0) pre_duration = len(song) song = song[start_trim:len(song) - end_trim] post_duration = len(song) log.info('silence trimmed', trim_start=duration_to_human(start_trim), trim_end=duration_to_human(end_trim), duration_before=duration_to_human(pre_duration), duration_after=duration_to_human(post_duration)) if not no_fade: fade_in_duration = 100 fade_out_duration = 1000 log.info('applying fade-in & fade-out...', fade_in=duration_to_human(fade_in_duration), fade_out=duration_to_human(fade_out_duration)) song = song.fade_in(fade_in_duration).fade_out(fade_out_duration) duartion = len(song) log.info('saving song...', mp3_file=mp3_file, duration=duration_to_human(duartion)) song.export(mp3_file, format="mp3")
def rename_song(mp3_file: str, artist: str, title: str) -> str: with wrap_context('renaming song', artist=artist, title=title, mp3_file=mp3_file): dirname, filename = os.path.split(mp3_file) if artist.strip(): new_filename = f'{artist.strip()} - {title.strip()}.mp3' else: new_filename = f'{title.strip()}.mp3' new_path = Path(dirname) / new_filename os.rename(mp3_file, new_path) log.info('song renamed', new_name=new_path) return new_path
def incoming_request(self) -> HttpRequest: with wrap_context('building incoming request'): headers_dict = { key: self.headers[key] for key in self.headers.keys() } method = self.command.upper() content_len = int(get_header(headers_dict, 'Content-Length', '0')) content: bytes = self.rfile.read( content_len) if content_len else b'' return HttpRequest(requestline=self.requestline, method=method, path=self.path, headers=headers_dict, content=content, client_addr=self.client_address[0], client_port=self.client_address[1], timestamp=now_seconds())
def trim_url(url: str, user_artist: Optional[str], user_title: Optional[str], no_trim: bool, no_fade: bool, no_normalize: bool, trim_start: Optional[float], trim_end: Optional[float], gain: Optional[float], output: Optional[str]): with wrap_context('url song'): yt_artist, yt_title = extract_youtube_artist_title(url) log.info('artist & title extracted from youtube page', artist=yt_artist, title=yt_title) artist = user_artist or enter_or_default('Artist', default=yt_artist) title = user_title or enter_or_default('Title', default=yt_title) log.info('song name set', name=f'{artist} - {title}') mp3_file = download_from_youtube(url) if output: mp3_file = rename_output_song(mp3_file, output) else: mp3_file = rename_song(mp3_file, artist, title) normalize_song(mp3_file, no_trim, no_fade, no_normalize, trim_start, trim_end, gain) tag_mp3(mp3_file, artist, title) log.info('song saved', mp3_file=mp3_file)
def trim_mp3(mp3_file: str, user_artist: Optional[str], user_title: Optional[str], no_trim: bool, no_fade: bool, no_normalize: bool, no_rename: bool, trim_start: Optional[float], trim_end: Optional[float], gain: Optional[float], output: Optional[str]): with wrap_context('mp3 song'): assert os.path.isfile(mp3_file), 'input file should exist' if output: shutil.copyfile(mp3_file, output) mp3_file = output tag_artist, tag_title = read_mp3_artist_title(mp3_file) artist = user_artist or tag_artist or enter_or_default('Artist', default='') title = user_title or tag_title or enter_or_default('Title', default='') if not output and not no_rename: mp3_file = rename_song(mp3_file, artist, title) normalize_song(mp3_file, no_trim, no_fade, no_normalize, trim_start, trim_end, gain) tag_mp3(mp3_file, artist, title) log.info('song saved', mp3_file=mp3_file)
def setup_proxy(listen_port: int, listen_ssl: bool, dst_url: str, record: bool, record_file: str, replay: bool, replay_throttle: bool, replay_clear_cache: bool, replay_clear_cache_seconds: int, config: str, verbose: int): with logerr(): with wrap_context('initialization'): extensions = load_extensions(config) _config = Config( listen_port=listen_port, listen_ssl=listen_ssl, dst_url=dst_url, record=record, record_file=record_file, replay=replay, replay_throttle=replay_throttle, replay_clear_cache=replay_clear_cache, replay_clear_cache_seconds=replay_clear_cache_seconds, verbose=verbose, ) if extensions.override_config: extensions.override_config(_config) log.info('Configuration set', **asdict(_config)) RequestHandler.extensions = extensions RequestHandler.config = _config RequestHandler.cache = RequestCache(extensions, _config) TCPServer.allow_reuse_address = True httpd = TCPServer((_config.listen_addr, _config.listen_port), RequestHandler) if _config.listen_ssl: httpd.socket = ssl.wrap_socket(httpd.socket, certfile='./dev-cert.pem', server_side=True) log.info( f'Listening on {_config.listen_scheme} port {_config.listen_port}...' ) try: httpd.serve_forever() finally: httpd.server_close()
def fetch_youtube_metadata(url: str) -> Tuple[str, str, str]: with wrap_context('fetching title from youtube', url=url): log.info('fetching metadata from youtube page...', url=url) ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl': '%(title)s.%(ext)s', } with youtube_dl.YoutubeDL(ydl_opts) as ydl: einfo = ydl.extract_info(url, download=False) track = einfo.get('track') artist = einfo.get('artist') or einfo.get('creator') full_title = einfo.get('title') or einfo.get('alt_title') log.info('youtube page metadata fetched', yt_title=full_title, artist=artist, track=track) return artist, track, full_title
def rename_output_song(mp3_file: str, output: str) -> str: with wrap_context('renaming song', mp3_file=mp3_file, output=output): os.rename(mp3_file, output) log.info('song renamed', new_name=output) return output
def extract_filename_artist_title(mp3_file: str) -> Tuple[str, str]: with wrap_context('extracting metadata from filename'): _, filename = os.path.split(mp3_file) if filename.lower().endswith('.mp3'): filename = mp3_file[:-4] return extract_artist_title(filename)