Skip to content

Commit

Permalink
Merge pull request #18 from HisAtri/ur2853956
Browse files Browse the repository at this point in the history
音乐Tag修改+/api/v1 端点
  • Loading branch information
HisAtri authored Jan 21, 2024
2 parents 08d010d + dfea294 commit 4736640
Show file tree
Hide file tree
Showing 21 changed files with 2,254 additions and 132 deletions.
2 changes: 2 additions & 0 deletions dockerignore → .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.git
.github
tests/
.deploy_tmp_path
32 changes: 25 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
# 基于Python3.9 alpine镜像
FROM python:3.9.17-alpine3.18
# 第一阶段:安装GCC
FROM python:3.12.1-alpine as gcc_installer

# 安装GCC
RUN apk add --no-cache gcc musl-dev

# 第二阶段:安装Python依赖
FROM gcc_installer as requirements_installer

# 设置工作目录
WORKDIR /app

# 将源代码复制到Docker镜像中
COPY ./LrcApi /app
# 只复制 requirements.txt,充分利用 Docker 缓存层
COPY ./LrcApi/requirements.txt /app/

# 安装Python依赖
RUN pip install --no-user --prefix=/install -r requirements.txt

# 第三阶段:运行环境
FROM python:3.12.1-alpine

# 安装Python项目依赖
RUN pip install -r /app/requirements.txt
# 设置工作目录
WORKDIR /app

# 复制Python依赖
COPY --from=requirements_installer /install /usr/local

# 复制项目代码
COPY ./LrcApi /app

# 设置启动命令
CMD ["python", "/app/app.py"]
CMD ["python", "/app/app.py"]
111 changes: 84 additions & 27 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import hashlib
import logging
import os
from urllib.parse import unquote_plus

import shutil
import hashlib
import logging
import requests
from flask import Flask, request, abort, redirect, send_from_directory, Response, jsonify, render_template_string, \
import concurrent.futures

from flask import Flask, Blueprint, request, abort, redirect, send_from_directory, jsonify, render_template_string, \
make_response
from flask_caching import Cache
from waitress import serve
import concurrent.futures
from urllib.parse import unquote_plus

from mod import search, lrc, tags
from mod import search, lrc
from mod import tag
from mod.auth import webui, cookie
from mod.auth.authentication import require_auth
from mod.args import GlobalArgs

args = GlobalArgs()
app = Flask(__name__)

v1_bp = Blueprint('v1', __name__, url_prefix='/api/v1')

# Blueprint直接复制app配置项
v1_bp.config = app.config.copy()

# 缓存逻辑
cache_dir = './flask_cache'
try:
# 尝试删除缓存文件夹
shutil.rmtree(cache_dir)
except FileNotFoundError:
pass
# 定义缓存逻辑为本地文件缓存,目录为cache_dir = './flask_cache'
cache = Cache(app, config={
'CACHE_TYPE': 'filesystem',
'CACHE_DIR': cache_dir
Expand Down Expand Up @@ -78,6 +85,7 @@ def read_file_with_encoding(file_path, encodings):


@app.route('/lyrics', methods=['GET'])
@v1_bp.route('/lyrics/single', methods=['GET'])
@cache.cached(timeout=86400, key_prefix=make_cache_key)
def lyrics():
match require_auth(request=request):
Expand All @@ -97,7 +105,7 @@ def lyrics():
if file_content is not None:
return lrc.standard(file_content)
try:
lrc_in = tags.r_lrc(path)
lrc_in = tag.tout(path).get("lyrics", "")
if type(lrc_in) is str and len(lrc_in) > 0:
return lrc_in
except:
Expand All @@ -117,6 +125,7 @@ def lyrics():


@app.route('/jsonapi', methods=['GET'])
@v1_bp.route('/lyrics/advance', methods=['GET'])
@cache.cached(timeout=86400, key_prefix=make_cache_key)
def lrc_json():
match require_auth(request=request):
Expand All @@ -126,8 +135,8 @@ def lrc_json():
return render_template_string(webui.error()), 421
if not bool(request.args):
abort(404, "请携带参数访问")
path = unquote_plus(request.args.get('path'))
title = unquote_plus(request.args.get('title'))
path = unquote_plus(request.args.get('path', ''))
title = unquote_plus(request.args.get('title', ''))
artist = unquote_plus(request.args.get('artist', ''))
album = unquote_plus(request.args.get('album', ''))
response = []
Expand All @@ -154,10 +163,13 @@ def lrc_json():
"artist": artist,
"lyrics": i
})
_response = jsonify(response)
_response.headers['Content-Type'] = 'application/json; charset=utf-8'
return jsonify(response)


@app.route('/cover', methods=['GET'])
@v1_bp.route('/cover', methods=['GET'])
@cache.cached(timeout=86400, key_prefix=make_cache_key)
def cover_api():
req_args = {key: request.args.get(key) for key in request.args}
Expand Down Expand Up @@ -185,9 +197,10 @@ def validate_json_structure(data):
return True


@app.route('/tag', methods=['POST'])
@app.route('/tag', methods=['GET', 'POST', 'PUT'])
@v1_bp.route('/tag', methods=['GET', 'POST', 'PUT'])
def setTag():
match require_auth(request=request):
match require_auth(request=request, permission='rw'):
case -1:
return render_template_string(webui.error()), 403
case -2:
Expand All @@ -205,41 +218,68 @@ def setTag():
return "File not found.", 404

supported_tags = {
"title": "title",
"artist": "artist",
"album": "album",
"lyrics": "lyrics"
"tracktitle": {"allow": (str, bool, type(None)), "caption": "Track Title"},
"artist": {"allow": (str, bool, type(None)), "caption": "Artists"},
"album": {"allow": (str, bool, type(None)), "caption": "Albums"},
"year": {"allow": (int, bool, type(None)), "caption": "Album year"},
"lyrics": {"allow": (str, bool, type(None)), "caption": "Lyrics text"}
}

tags_to_set = {supported_tags[key]: value for key, value in musicData.items() if key in supported_tags}
result = tags.w_file(audio_path, tags_to_set)
if result == 0:
return "OK", 200
elif result == -1:
return "Failed to write lyrics", 523
elif result == -2:
return "Failed to write tags", 524
else:
return "Unknown error", 525
if "title" in musicData and "tracktitle" not in musicData:
musicData["tracktitle"] = musicData["title"]
tags_to_set = {}
for key, value in musicData.items():
if key in supported_tags and isinstance(value, supported_tags[key]["allow"]):
tags_to_set[key] = value
try:
tag.tin(tags=tags_to_set, file=audio_path)
except TypeError as e:
return str(e), 524
except FileNotFoundError as e:
return str(e), 404
except Exception as e:
return str(e), 500
return "Succeed", 200


@app.route('/')
def redirect_to_welcome():
"""
重定向至/src,显示主页
:return:
"""
return redirect('/src')


@app.route('/favicon.ico')
def favicon():
"""
favicon位置,返回图片
:return:
"""
return send_from_directory('src', 'img/Logo_Design.svg')


@app.route('/src')
def return_index():
"""
显示主页
:return: index page
"""
return send_from_directory('src', 'index.html')


@app.route('/src/<path:filename>')
def serve_file(filename):
"""
路由/src/
路径下的静态资源
:param filename:
:return:
"""
FORBIDDEN_EXTENSIONS = ('.exe', '.bat', '.dll', '.sh', '.so', '.php', '.sql', '.db', '.mdb', '.gz', '.tar', '.bak',
'.tmp', '.key', '.pem', '.crt', '.csr', '.log')
if filename.lower().endswith(FORBIDDEN_EXTENSIONS):
abort(404)
try:
return send_from_directory('src', filename)
except FileNotFoundError:
Expand All @@ -248,6 +288,11 @@ def serve_file(filename):

@app.route('/login')
def login_check():
"""
登录页面
未登录时返回页面,已登录时重定向至主页
:return:
"""
if require_auth(request=request) < 0 and args.auth:
return render_template_string(webui.html_login())

Expand All @@ -256,6 +301,12 @@ def login_check():

@app.route('/login-api', methods=['POST'])
def login_api():
"""
登录对接的API
包括验证和下发Cookie
success提示成功与否
:return:
"""
data = request.get_json()
if 'password' in data:
pwd = data['password']
Expand All @@ -269,12 +320,18 @@ def login_api():


def main():
# Waitress WSGI 服务器
serve(app, host=args.ip, port=args.port, threads=32, channel_timeout=30)
# Debug服务器
# app.run(host='0.0.0.0', port=args.port)


if __name__ == '__main__':
# 日志配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('')
logger.info("正在启动服务器")
# 注册 Blueprint 到 Flask 应用
app.register_blueprint(v1_bp)
# 启动
main()
81 changes: 81 additions & 0 deletions mod/music_tag/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python
# This whole module is just id3 tag part of MusicBrainz Picard
# The idea is to get Musicbrainz's layer on top of mutagen without
# dependancies (like qt)

import logging
import os

import mutagen

from . import file
from . import util
from . import aac
from . import aiff
from . import apev2
from . import asf
from . import dsf
from . import flac
from . import id3
from . import mp4
from . import smf
from . import vorbis
from . import wave

from .file import Artwork, MetadataItem, NotAppendable, AudioFile

__version__ = """0.4.3"""

logger = logging.getLogger("music_tag")
log = logger


def _subclass_spider_dfs(kls, _lst=None):
if _lst is None:
_lst = []
for sub in kls.__subclasses__():
_subclass_spider_dfs(sub, _lst=_lst)
_lst.append(kls)
return _lst


def load_file(file_spec, err='raise'):
if isinstance(file_spec, mutagen.FileType):
mfile = file_spec
filename = mfile.filename
else:
filename = file_spec
if not os.path.exists(filename):
if os.path.exists(os.path.expanduser(os.path.expandvars(filename))):
filename = os.path.expanduser(os.path.expandvars(filename))
elif os.path.exists(os.path.expanduser(filename)):
filename = os.path.expanduser(filename)
mfile = mutagen.File(filename, easy=False)

ret = None

for kls in _subclass_spider_dfs(file.AudioFile):
# print("checking against:", kls, kls.mutagen_kls)
if kls.mutagen_kls is not None and isinstance(mfile, kls.mutagen_kls):
ret = kls(filename, _mfile=mfile)
break

if ret is None and err == 'raise':
raise NotImplementedError("Mutagen type {0} not implemented"
"".format(type(mfile)))

return ret


__all__ = ['file', 'util',
'aac', 'aiff', 'apev2', 'asf', 'dsf', 'flac',
'id3', 'mp4', 'smf', 'vorbis', 'wave',
'logger', 'log',
'Artwork', 'MetadataItem', 'NotAppendable',
'AudioFile',
'load_file',
]

##
## EOF
##
Loading

0 comments on commit 4736640

Please sign in to comment.