def playlist(playlist_slug): """Add song to playlist.""" try: playlist = Playlist.get(playlist_slug) except DoesNotExist as e: sys.exit(e) songs = [song.slug for song in Song.all() if song not in playlist] try: song_slug = subprocess.run( ['fzf', '--no-sort', '--exact'], input='\n'.join(songs), universal_newlines=True, stdout=subprocess.PIPE, ).stdout.strip() except FileNotFoundError as e: sys.exit(e) if not song_slug: return song = Song.get(song_slug) # If the user selects nothing we leave the root empty, and the # default root is used. If he wants to explicitly set the default # root in case it's changed in the future, he has to select it. default = song.scale[0:2].strip() root = click.prompt(f'Root [{default}]', default='', show_default=False) if root and root not in SHARPS + FLATS: sys.exit(f'Invalid root: {root}') playlist.add(song_slug, root)
def delete(slug): try: song = Song.get(slug) except DoesNotExist: abort(404) song.delete() return redirect(url_for('main.index'))
def edit(slug): form = SongForm(request.form) song = Song.get(slug) if request.method == 'POST' and form.validate(): song.name = form.name.data song.year = form.year.data song.artist = form.artist.data song.body = form.body.data song.scale = form.scale.data song.rhythm = form.rhythm.data song.link = form.link.data song.tofile() return redirect(url_for('main.song', slug=song.slug)) # Populate form with song's attributes form.name.data = song.name form.year.data = song.year form.artist.data = song.artist form.body.data = song.body form.scale.data = song.scale form.rhythm.data = song.rhythm form.link.data = song.link return render_template( 'admin/songform.html', form=form, action=url_for('admin.edit', slug=slug), title=song.name, )
def test_add(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) playlist.tofile() SongFactory(name='Ασδφ', artist='Ασδφ').tofile() playlist.add('asdf') assert (playlist.directory / f'{playlist.slug}.yml').read_text() == dedent("""\ name: Λίστα songs: - name: Ασδφ slug: asdf artist: Ασδφ artist_slug: asdf - name: Νέημ slug: neim artist: Βαμβακάρης artist_slug: vamvakaris root: A - name: Τεστ slug: test artist: Βαμβακάρης artist_slug: vamvakaris """)
def test_all(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) playlist.tofile() assert Playlist.all() == [playlist]
def export(): """Export all playlist's songs to file.""" playlist = Playlist.get('giannis') with open('asdf.txt', 'w') as f: for song in playlist.songs: song = Song.get(song.slug, root=song.root) f.write(f"{song.name} - {song.artist} ({song.year})\n\n") f.write(f"{song.info()}\n\f")
def index(): """A list of all songs in the database.""" return render_template( 'index.html', title='Τραγούδια', songs=Song.all(), admin=session.get('logged_in'), )
def test_save_delete(client): with client.session_transaction() as session: session['logged_in'] = True song = SongFactory(body='Bm F# Bm') song.tofile() assert len(Song.all()) == 1 url = url_for('admin.save', slug='name', semitones=1) resp = client.get(url, follow_redirects=True) assert resp.status_code == 200 song = Song.get('name') assert song.body == 'Cm G Cm' resp = client.get(url_for('admin.delete', slug='name'), follow_redirects=True) assert Song.all() == [] resp = client.get(url_for('admin.delete', slug='name'), follow_redirects=True) assert 'Δεν υπάρχει τέτοια σελίδα'.encode() in resp.data
def add(self, song_slug, root=None): if root and not re.match('^[A-G][bs]?$', root): raise InvalidNote(f"'{root}' is not a valid note") self.songs = [song for song in self.songs if song.slug != song_slug] song = Song.get(song_slug, root=root) self.songs.append(song) self.songs.sort(key=lambda song: unaccented(song.name)) self.tofile()
def index(): """A list of all songs in the database.""" songs = Song.all() return render_template( 'index.html', title='Admin', songs=songs, admin=True, )
def test_add_invalid_root(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) playlist.tofile() SongFactory(name='Ασδφ', artist='Ασδφ').tofile() with pytest.raises(InvalidNote): playlist.add('asdf', root='L')
def test_init(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) assert playlist.name == 'Λίστα' assert playlist.slug == 'lista' assert playlist.songs == songs assert playlist.num == 2
def filenames(): """Check that song filenames and slugs match.""" directory: Path = app.config['DIR'] / 'songs' assert directory.is_dir() path: Path for path in directory.iterdir(): song = Song.fromfile(path.name) if path.name != song.slug: if click.confirm(f"{path.name} -> {song.slug}?"): new_path = directory / song.slug path.rename(new_path)
def complement(slug): """A list of all songs in a given playlist.""" playlist = Playlist.get_or_404(slug) all_songs = Song.all() songs = [song for song in all_songs if song not in playlist.songs] return render_template( 'index.html', title=playlist.name, songs=songs, admin=session.get('logged_in'), )
def SongFactory(**kwargs): return Song( name=kwargs.get('name', 'name'), year=kwargs.get('year', None), artist=kwargs.get('artist', 'artist'), link=kwargs.get('link', 'link'), scale=kwargs.get('scale', 'scale'), rhythm=kwargs.get('rhythm', 'rhythm'), body=kwargs.get('body', 'body'), root=kwargs.get('root', None), )
def test_contains(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) playlist.tofile() assert 'test' in playlist assert SongFactory(name='Τεστ') in playlist assert 'asdf' not in playlist assert SongFactory(name='Ασδφ') not in playlist
def index(): """Index all data into elasticsearch.""" elastic.create_index() def index_all(items, name): with click.progressbar(items, label=f"Indexing {name}") as bar: for item in bar: elastic.index(item) index_all(Song.all(frommetadata=False), 'songs') index_all(Artist.all(), 'artists') index_all(Scale.all(), 'scales')
def song(slug, semitones=None, root=None): """A song optionally transposed by given semitones.""" try: song = Song.get(slug, semitones=semitones, root=root, unicode=True) except DoesNotExist: abort(404) except InvalidNote as e: return jsonify({'message': str(e)}), 400 return jsonify({ 'name': song.name, 'artist': song.artist, 'link': song.link, 'info': song.info(html=True), })
def random(): """Redirect to a random song. The song is chosen randomly from the selected playlist, or the whole database, if no playlist is selected. The last accessed songs, which are located in the 'latest_songs' cookie, via the `add_slug_to_cookie` decorator, are excluded from the selection. """ playlist_slug = request.cookies.get('playlist') songs = Playlist.get(playlist_slug).songs if playlist_slug else Song.all() cookie = request.cookies.get('latest_songs') latest_songs = json.loads(cookie) if cookie else [] song = choice([song for song in songs if song.slug not in latest_songs]) return redirect(url_for('main.song', slug=song.slug) + '?random=true')
def add(): """Add a new song to the database.""" form = SongForm(request.form) if request.method == 'POST': if form.validate(): song = Song( name=form.name.data, year=form.year.data, artist=form.artist.data, scale=form.scale.data, rhythm=form.rhythm.data, body=form.body.data, link=form.link.data, ) song.tofile() return redirect(url_for('main.song', slug=song.slug)) return render_template( 'admin/songform.html', form=form, action=url_for('admin.add'), title="Νέο τραγούδι", )
def scale(slug, root='D'): """A list of all available scales.""" if not re.match('^[A-G][bs]?$', root): abort(404) root = re.sub('s', '#', root) scale = Scale.get_or_404(slug) scale.root = root songs = [song for song in Song.all() if scale.name in song.scale] scales = [s for s in Scale.all() if s.slug != scale.slug] return render_template( 'scale.html', scale=scale, songs=songs, scales=scales, admin=session.get('logged_in'), )
def get(cls, slug): """Playlist constructor that takes the name of the scale.""" path: Path = app.config['DIR'] / 'playlists' / f'{slug}.yml' try: data = yaml.safe_load(path.read_text()) except FileNotFoundError: raise DoesNotExist(f"Playlist '{slug}' does not exist") songs = [] roots = {} for song_data in data['songs']: songs.append(Song.frommetadata(song_data)) if 'root' in song_data: roots[song_data['slug']] = song_data['root'] return cls(name=data['name'], songs=songs, roots=roots)
def test_prepare_song(client): song1 = SongFactory(name='name_a', scale='D#') song2 = SongFactory(name='name_b', scale='Eb') song1.tofile() song2.tofile() assert Song.get('name_a', unicode=True).scale == 'D♯' assert Song.get('name_b', unicode=True).scale == 'E♭' assert Song.get('name_a', semitones=-2).scale == 'C#' assert Song.get('name_b', semitones=-2).scale == 'Db' assert Song.get('name_a', root='B').scale == 'B' assert Song.get('name_b', root='B').scale == 'B'
def test_remove(client): songs = [ Song.frommetadata(song_data) for song_data in playlist_data['songs'] ] playlist = Playlist(name=playlist_data['name'], songs=songs) playlist.tofile() playlist.remove('neim') assert (playlist.directory / f'{playlist.slug}.yml').read_text() == dedent("""\ name: Λίστα songs: - name: Τεστ slug: test artist: Βαμβακάρης artist_slug: vamvakaris """)
def wrapper(*args, **kwargs): playlist = get_selected_playlist() num_songs = playlist.num if playlist else len(Song.all()) limit = int(0.9 * num_songs) response = make_response(f(*args, **kwargs)) cookie = request.cookies.get('latest_songs') latest_songs = json.loads(cookie) if cookie else [] slug = kwargs['slug'] latest_songs.sort(key=slug.__eq__) # Move to end if not latest_songs or latest_songs[-1] != slug: latest_songs.append(slug) len_cookie = len(latest_songs) if len_cookie > limit: latest_songs = latest_songs[len_cookie - limit:] response.set_cookie('latest_songs', json.dumps(latest_songs)) return response
def test_add(client): with client.session_transaction() as session: session['logged_in'] = True resp = client.get(url_for('admin.add')) assert 'Νέο τραγούδι'.encode() in resp.data resp = client.post( url_for('admin.add'), data={ 'name': 'name', 'artist': 'artist', 'scale': 'scale', 'rhythm': 'rhythm', 'body': 'body', 'link': 'https://www.youtube.com/watch?v=asdfasdf', }, follow_redirects=True ) song = Song.get('name') assert song.name == 'name'
def song(slug, semitones=None, root=None): """A song optionally transposed by given semitones.""" logging.info(f"Got request for song {slug}") playlist: Playlist = get_selected_playlist() if playlist and not root: try: root = playlist.roots[slug] except KeyError: pass logging.debug("Got selected playlist") if session.get('logged_in') and request.args.get('random') != 'true': Session.get().add_song(slug) logging.debug("Added song to session") song = Song.get_or_404(slug, semitones=semitones, root=root, unicode=True) artist = Artist.get(song.artist_slug) logging.debug("Read song data from disk") related_songs = get_related(slug) if related_songs is not None: related_title = 'Σχετικά' else: related_songs = [song for song in artist.songs if song.slug != slug] related_title = ('Άλλα παραδοσιακά' if song.artist == 'Παραδοσιακό' else f'Άλλα του {artist.genitive}') logging.debug("Rendering\n") return render_template( 'song.html', song=song, artist=artist, songs=related_songs, related_title=related_title, semitones=semitones, root=root, admin=session.get('logged_in'), )
def search(): query = request.args.get('q') if not query: return redirect(url_for('main.index')) songs = list(Song.search(query)) if len(songs) > 1: return render_template( 'index.html', title=query, songs=songs, admin=session.get('logged_in'), ) if len(songs) == 1: return redirect(url_for('main.song', slug=songs[0].slug)) artists = list(Artist.search(query)) if len(artists) > 1: return render_template( 'list.html', title=query, objects=artists, detail_url='main.artist', admin=session.get('logged_in'), ) if len(artists) == 1: return redirect(url_for('main.artist', slug=artists[0].slug)) if any(scale.name == query for scale in Scale.all()): scale = Scale.get(query) return redirect(url_for('main.scale', slug=scale.slug)) abort(404)
def check(): """Check and fix audio files.""" def clear_metadata(filename): audio = ID3(filename) keys = list(audio.keys()) if keys: for key in keys: audio.delall(key) audio.save() def set_comment(filename, comment): audio = ID3(filename) audio.add(COMM(encoding=3, text=comment)) audio.save() def get_comment(filename): audio = ID3(filename) comments = audio.getall("COMM") if not comments: return return comments[0].text[0] def download(song): """Download `song`'s YouTube video and convert to mp3.""" filename = '' def download_hook(d): nonlocal filename if d['status'] == 'finished': filename = d['filename'] print("Done downloading, now converting...") ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'progress_hooks': [download_hook], } with youtube_dl.YoutubeDL(ydl_opts) as ydl: try: ydl.download([song.link]) except youtube_dl.DownloadError as e: print( click.style(f"Couldn't download {song.name}: {e}", fg='bright_red')) return path = Path(filename).with_suffix('.mp3') path.rename(song.audio_path) set_comment(song.audio_path, song.youtube_id) def check_link(song): """Return whether `song`'s YouTube link is still valid.""" url = f'http://img.youtube.com/vi/{song.youtube_id}/mqdefault.jpg' try: response = requests.get(url) except requests.ConnectionError as e: sys.exit(e) if response.status_code != 200: return False return True directory: Path = app.config['DIR'] / 'songs' songs = [Song.get(path.name) for path in directory.iterdir()] songs.sort(key=lambda song: unaccented(song.name)) num = len(songs) for i, song in enumerate(songs, start=1): print(f"{i}/{num}", end='\t') if song.youtube_id: if not check_link(song): assert song.audio_path.is_file() print( click.style(f"{song.name} has invalid youtube id", fg='bright_red')) elif song.audio_path.is_file(): youtube_id = get_comment(song.audio_path) if song.youtube_id != youtube_id: print(f"{song.name} had changed " f"({song.youtube_id} != {youtube_id})") if click.confirm("Replace?"): song.audio_path.unlink() print(f"Downloading {song.name}...") download(song) print() else: print(song.name, "is OK") else: print(f"Downloading {song.name}...") download(song) print() else: assert song.audio_path.is_file() print(song.name, "has no youtube id but audio is downloaded") clear_metadata(song.audio_path) if not song.year: print(click.style(f"{song.name} has no year", fg='bright_red'))
def save(slug, semitones=None, root=None): """Save a transposed song to the database.""" song = Song.get(slug, semitones=semitones, root=root) song.tofile() return redirect(url_for('main.song', slug=song.slug))