Пример #1
0
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)
Пример #2
0
def delete(slug):
    try:
        song = Song.get(slug)
    except DoesNotExist:
        abort(404)
    song.delete()
    return redirect(url_for('main.index'))
Пример #3
0
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,
    )
Пример #4
0
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
        """)
Пример #5
0
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]
Пример #6
0
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")
Пример #7
0
def index():
    """A list of all songs in the database."""
    return render_template(
        'index.html',
        title='Τραγούδια',
        songs=Song.all(),
        admin=session.get('logged_in'),
    )
Пример #8
0
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
Пример #9
0
    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()
Пример #10
0
def index():
    """A list of all songs in the database."""
    songs = Song.all()
    return render_template(
        'index.html',
        title='Admin',
        songs=songs,
        admin=True,
    )
Пример #11
0
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')
Пример #12
0
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
Пример #13
0
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)
Пример #14
0
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'),
    )
Пример #15
0
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),
    )
Пример #16
0
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
Пример #17
0
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')
Пример #18
0
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),
    })
Пример #19
0
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')
Пример #20
0
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="Νέο τραγούδι",
    )
Пример #21
0
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'),
    )
Пример #22
0
    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)
Пример #23
0
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'
Пример #24
0
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
        """)
Пример #25
0
    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
Пример #26
0
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'
Пример #27
0
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'),
    )
Пример #28
0
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)
Пример #29
0
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'))
Пример #30
0
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))