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
|
### 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):
|
||||||
|
2
setup.py
2
setup.py
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user