1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-20 19:31:04 +00:00

Use mediafile rather than mutagen directly

This commit is contained in:
Alban Féron 2020-10-24 17:55:21 +02:00
parent b07babb4ff
commit 0183bcb698
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
6 changed files with 39 additions and 120 deletions

View File

@ -56,19 +56,10 @@ but not both.
### Prerequisites
You'll need these to run _Supysonic_:
You'll need Python 3.5 or later to run _Supysonic_.
* Python >= 3.5
* [Flask](http://flask.pocoo.org/)
* [PonyORM](https://ponyorm.com/)
* [Python Imaging Library](https://github.com/python-pillow/Pillow)
* [requests](http://docs.python-requests.org/)
* [mutagen](https://mutagen.readthedocs.io/en/latest/)
* [watchdog](https://github.com/gorakhargosh/watchdog)
* [zipstream](https://github.com/allanlei/python-zipstream)
All the dependencies will automatically be installed by the
installation command above.
All the dependencies will automatically be installed by the installation
command above.
You may also need a database specific package if you don't want to use SQLite
(the default):

View File

@ -19,7 +19,7 @@ reqs = [
"pony>=0.7.6",
"Pillow",
"requests>=1.0.0",
"mutagen>=1.33",
"mediafile",
"watchdog>=0.8.0",
"zipstream",
]

View File

@ -3,21 +3,22 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2013-2019 Alban 'spl0k' Féron
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
# 2018-2019 Carey 'pR0Ps' Metcalfe
#
# Distributed under terms of the GNU AGPLv3 license.
import hashlib
import io
import json
import logging
import mediafile
import mimetypes
import os.path
import requests
import shlex
import subprocess
import uuid
import io
import hashlib
import json
import zlib
from flask import request, Response, send_file
@ -30,7 +31,6 @@ from zipstream import ZipFile
from .. import scanner
from ..cache import CacheMiss
from ..covers import get_embedded_cover
from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import api, get_entity, get_entity_id
@ -267,8 +267,9 @@ def cover_art():
cover_path = cache.get(cache_key)
except CacheMiss:
res = get_entity(Track)
art = get_embedded_cover(res.path)
if not art:
try:
art = mediafile.MediaFile(res.path).art
except mediafile.UnreadableFileError:
raise NotFound("Cover art")
cover_path = cache.set(cache_key, art)
else:

View File

@ -17,9 +17,7 @@ from .exceptions import Forbidden, MissingParameter, NotFound
@api.route("/getInternetRadioStations.view", methods=["GET", "POST"])
def get_radio_stations():
query = RadioStation.select().sort_by(
RadioStation.name
)
query = RadioStation.select().sort_by(RadioStation.name)
return request.formatter(
"internetRadioStations",
dict(internetRadioStation=[p.as_subsonic_station() for p in query]),
@ -31,7 +29,9 @@ def create_radio_station():
if not request.user.admin:
raise Forbidden()
stream_url, name, homepage_url = map(request.values.get, ["streamUrl", "name", "homepageUrl"])
stream_url, name, homepage_url = map(
request.values.get, ["streamUrl", "name", "homepageUrl"]
)
if stream_url and name:
RadioStation(stream_url=stream_url, name=name, homepage_url=homepage_url)
@ -48,7 +48,9 @@ def update_radio_station():
res = get_entity(RadioStation)
stream_url, name, homepage_url = map(request.values.get, ["streamUrl", "name", "homepageUrl"])
stream_url, name, homepage_url = map(
request.values.get, ["streamUrl", "name", "homepageUrl"]
)
if stream_url and name:
res.stream_url = stream_url
res.name = name
@ -70,4 +72,3 @@ def delete_radio_station():
res.delete()
return request.formatter.empty

View File

@ -11,11 +11,6 @@ import os.path
import re
import warnings
from base64 import b64decode
from mutagen import File, FileType
from mutagen.easyid3 import EasyID3
from mutagen.flac import FLAC, Picture
from mutagen._vorbis import VCommentDict
from PIL import Image
from os import scandir
@ -90,48 +85,3 @@ def find_cover_in_folder(path, album_name=None):
return candidates[0]
return sorted(candidates, key=lambda c: c.score, reverse=True)[0]
def get_embedded_cover(path):
if not isinstance(path, str): # pragma: nocover
raise TypeError("Expecting string, got " + str(type(path)))
if not os.path.exists(path):
return None
metadata = File(path, easy=True)
if not metadata:
return None
if isinstance(metadata.tags, EasyID3):
picture = metadata["pictures"][0]
elif isinstance(metadata, FLAC):
picture = metadata.pictures[0]
elif isinstance(metadata.tags, VCommentDict):
picture = Picture(b64decode(metadata.tags["METADATA_BLOCK_PICTURE"][0]))
else:
return None
return picture.data
def has_embedded_cover(metadata):
if not isinstance(metadata, FileType): # pragma: nocover
raise TypeError("Expecting mutagen.FileType, got " + str(type(metadata)))
pictures = []
if isinstance(metadata.tags, EasyID3):
pictures = metadata.get("pictures", [])
elif isinstance(metadata, FLAC):
pictures = metadata.pictures
elif isinstance(metadata.tags, VCommentDict):
pictures = metadata.tags.get("METADATA_BLOCK_PICTURE", [])
return len(pictures) > 0
def _get_id3_apic(id3, key):
return id3.getall("APIC")
EasyID3.RegisterKey("pictures", _get_id3_apic)

View File

@ -3,13 +3,13 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2013-2019 Alban 'spl0k' Féron
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
import logging
import os, os.path
import mutagen
import mediafile
import time
from datetime import datetime
@ -17,7 +17,7 @@ from pony.orm import db_session
from queue import Queue, Empty as QueueEmpty
from threading import Thread, Event
from .covers import find_cover_in_folder, has_embedded_cover, CoverFile
from .covers import find_cover_in_folder, CoverFile
from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack
@ -233,32 +233,19 @@ class Scanner(Thread):
trdict = {"path": path}
artist = self.__try_read_tag(tag, "artist", "[unknown]")[:255]
album = self.__try_read_tag(tag, "album", "[non-album tracks]")[:255]
albumartist = self.__try_read_tag(tag, "albumartist", artist)[:255]
artist = (self.__sanitize_str(tag.artist) or "[unknown]")[:255]
album = (self.__sanitize_str(tag.album) or "[non-album tracks]")[:255]
albumartist = (self.__sanitize_str(tag.albumartist) or artist)[:255]
trdict["disc"] = self.__try_read_tag(
tag, "discnumber", 1, lambda x: int(x.split("/")[0])
)
trdict["number"] = self.__try_read_tag(
tag, "tracknumber", 1, lambda x: int(x.split("/")[0])
)
trdict["title"] = self.__try_read_tag(tag, "title", basename)[:255]
trdict["year"] = self.__try_read_tag(
tag, "date", None, lambda x: int(x.split("-")[0])
)
trdict["genre"] = self.__try_read_tag(tag, "genre")
trdict["duration"] = int(tag.info.length)
trdict["has_art"] = has_embedded_cover(tag)
trdict["disc"] = tag.disc or 1
trdict["number"] = tag.track or 1
trdict["title"] = (self.__sanitize_str(tag.title) or basename)[:255]
trdict["year"] = tag.year
trdict["genre"] = tag.genre
trdict["duration"] = int(tag.length)
trdict["has_art"] = bool(tag.images)
trdict["bitrate"] = (
int(
tag.info.bitrate
if hasattr(tag.info, "bitrate")
else size * 8 / tag.info.length
)
// 1000
)
trdict["bitrate"] = tag.bitrate
trdict["last_modification"] = mtime
tralbum = self.__find_album(albumartist, album)
@ -431,25 +418,14 @@ class Scanner(Thread):
def __try_load_tag(self, path):
try:
return mutagen.File(path, easy=True)
except mutagen.MutagenError:
return mediafile.MediaFile(path)
except mediafile.UnreadableFileError:
return None
def __try_read_tag(self, metadata, field, default=None, transform=None):
try:
value = metadata[field][0]
value = value.replace("\x00", "").strip()
if not value:
return default
if transform:
value = transform(value)
return value if value else default
# KeyError: missing tag
# IndexError: tag is present but doesn't have any value
# ValueError: tag can't be transformed to correct type
except (KeyError, IndexError, ValueError):
return default
def __sanitize_str(self, value):
if value is None:
return None
return value.replace("\x00", "").strip()
def stats(self):
return self.__stats