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 ### Prerequisites
You'll need these to run _Supysonic_: You'll need Python 3.5 or later to run _Supysonic_.
* Python >= 3.5 All the dependencies will automatically be installed by the installation
* [Flask](http://flask.pocoo.org/) command above.
* [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.
You may also need a database specific package if you don't want to use SQLite You may also need a database specific package if you don't want to use SQLite
(the default): (the default):

View File

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

View File

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

View File

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

View File

@ -11,11 +11,6 @@ import os.path
import re import re
import warnings 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 PIL import Image
from os import scandir from os import scandir
@ -90,48 +85,3 @@ def find_cover_in_folder(path, album_name=None):
return candidates[0] return candidates[0]
return sorted(candidates, key=lambda c: c.score, reverse=True)[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. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
import logging import logging
import os, os.path import os, os.path
import mutagen import mediafile
import time import time
from datetime import datetime from datetime import datetime
@ -17,7 +17,7 @@ from pony.orm import db_session
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty
from threading import Thread, Event 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 Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack from .db import RatingFolder, RatingTrack
@ -233,32 +233,19 @@ class Scanner(Thread):
trdict = {"path": path} trdict = {"path": path}
artist = self.__try_read_tag(tag, "artist", "[unknown]")[:255] artist = (self.__sanitize_str(tag.artist) or "[unknown]")[:255]
album = self.__try_read_tag(tag, "album", "[non-album tracks]")[:255] album = (self.__sanitize_str(tag.album) or "[non-album tracks]")[:255]
albumartist = self.__try_read_tag(tag, "albumartist", artist)[:255] albumartist = (self.__sanitize_str(tag.albumartist) or artist)[:255]
trdict["disc"] = self.__try_read_tag( trdict["disc"] = tag.disc or 1
tag, "discnumber", 1, lambda x: int(x.split("/")[0]) trdict["number"] = tag.track or 1
) trdict["title"] = (self.__sanitize_str(tag.title) or basename)[:255]
trdict["number"] = self.__try_read_tag( trdict["year"] = tag.year
tag, "tracknumber", 1, lambda x: int(x.split("/")[0]) trdict["genre"] = tag.genre
) trdict["duration"] = int(tag.length)
trdict["title"] = self.__try_read_tag(tag, "title", basename)[:255] trdict["has_art"] = bool(tag.images)
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["bitrate"] = ( trdict["bitrate"] = tag.bitrate
int(
tag.info.bitrate
if hasattr(tag.info, "bitrate")
else size * 8 / tag.info.length
)
// 1000
)
trdict["last_modification"] = mtime trdict["last_modification"] = mtime
tralbum = self.__find_album(albumartist, album) tralbum = self.__find_album(albumartist, album)
@ -431,25 +418,14 @@ class Scanner(Thread):
def __try_load_tag(self, path): def __try_load_tag(self, path):
try: try:
return mutagen.File(path, easy=True) return mediafile.MediaFile(path)
except mutagen.MutagenError: except mediafile.UnreadableFileError:
return None return None
def __try_read_tag(self, metadata, field, default=None, transform=None): def __sanitize_str(self, value):
try: if value is None:
value = metadata[field][0] return None
value = value.replace("\x00", "").strip() return 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 stats(self): def stats(self):
return self.__stats return self.__stats