mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 08:56:17 +00:00
Use mediafile rather than mutagen directly
This commit is contained in:
parent
b07babb4ff
commit
0183bcb698
15
README.md
15
README.md
@ -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):
|
||||
|
2
setup.py
2
setup.py
@ -19,7 +19,7 @@ reqs = [
|
||||
"pony>=0.7.6",
|
||||
"Pillow",
|
||||
"requests>=1.0.0",
|
||||
"mutagen>=1.33",
|
||||
"mediafile",
|
||||
"watchdog>=0.8.0",
|
||||
"zipstream",
|
||||
]
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user