diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..154bfa6 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +#Github的token +GH_TOKEN=XX +#turso服务的url +TURSO_DB_URL=XX +#turso服务的认证token +TURSO_DB_AUTH_TOKEN=XX \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..90be523 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + allow: + - dependency-name: "Flask" + - dependency-name: "libsql_client" \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..fa4e3b7 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,44 @@ +# Configuration for Release Drafter: https://github.com/toolmantim/release-drafter +name-template: 'v$NEXT_PATCH_VERSION 🌈' +tag-template: 'v$NEXT_PATCH_VERSION' +version-template: $MAJOR.$MINOR.$PATCH +# Emoji reference: https://gitmoji.carloscuesta.me/ +categories: + - title: '✨ Features' + labels: + - 'feat' + - 'feature' + - 'enhancement' + - 'kind/feature' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'regression' + - 'kind/bug' + - title: 📝 Documentation updates + labels: + - 'documentation' + - 'kind/doc' + - 'doc' + - title: 👻 Maintenance + labels: + - 'chore' + - 'dependencies' + - 'kind/chore' + - 'kind/dep' + - title: ⚡️ Tests + labels: + - 'test' + - 'tests' +exclude-labels: + - 'reverted' + - 'no-changelog' + - 'skip-changelog' + - 'invalid' +change-template: '* $TITLE (#$NUMBER) @$AUTHOR' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +template: | + ## What’s Changed + $CHANGES \ No newline at end of file diff --git a/.github/workflows/update_release_draft.yml b/.github/workflows/update_release_draft.yml new file mode 100644 index 0000000..76de6e0 --- /dev/null +++ b/.github/workflows/update_release_draft.yml @@ -0,0 +1,24 @@ +name: update_release_draft.yml + +on: + push: + branches: + - main + pull_request: + + types: [ opened, reopened, synchronize ] + +permissions: + contents: write + packages: write + +jobs: + update_release_draft: + permissions: + contents: write # for release-drafter/release-drafter to create a github release + pull-requests: write # for release-drafter/release-drafter to add label to PR + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e55a3e1..6ae386b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e119425..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // 使用 IntelliSense 了解相关属性。 - // 悬停以查看现有属性的描述。 - // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: 当前文件", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "justMyCode": true - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 457f44d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.typeCheckingMode": "basic" -} \ No newline at end of file diff --git a/components/trakt/__init__.py b/components/trakt/__init__.py index 9481297..34fa04e 100644 --- a/components/trakt/__init__.py +++ b/components/trakt/__init__.py @@ -5,95 +5,150 @@ # 创建蓝图 blueprint = Blueprint('trakt', __name__, url_prefix='/trakt') + # =========================电影================================ @blueprint.route('/movie') def movies(): - """分页获取电影列表 + """分页获取电影列表 - Returns: - _type_: 电影列表 - """ - # 获取请求参数page和page_size - curr_page = int(request.args.get('page', 1)) - page_size = int(request.args.get('page_size', 10)) - try: - return Result.success(movie_handler.get_movies(curr_page, page_size)) - except Exception as e: - return Result.fail(e) + Returns: + _type_: 电影列表 + """ + # 获取请求参数page和page_size + curr_page = int(request.args.get('page', 1)) + page_size = int(request.args.get('page_size', 10)) + try: + return Result.success(movie_handler.get_movies(curr_page, page_size)) + except Exception as e: + return Result.fail(e) @blueprint.route('/movie/') def movie(movie_id): - """获取电影详情 + """获取电影详情 - Args: - movie_id (str): 电影的tmdb_id + Args: + movie_id (str): 电影的tmdb_id - Returns: - _type_: 电影详情 - """ - try: - return Result.success(movie_handler.get_movie(movie_id)) - except Exception as e: - return Result.fail(e) + Returns: + _type_: 电影详情 + """ + try: + return Result.success(movie_handler.get_movie(movie_id)) + except Exception as e: + return Result.fail(e) @blueprint.route('/update_movie_share_link', methods=['POST']) def update_movie_share_link(): - """更新电影分享链接 content-type为application/x-www-form-urlencoded - - Returns: - _type_: 更新结果 - """ - # 获取请求参数tmdb_id和share_link - movie_id = request.form.get('movie_id') - share_link = request.form.get('share_link') - if not movie_id or not share_link: - return Result.fail('movie_id和share_link不能为空') - try: - movie_handler.update_share_link(movie_id, share_link) - return Result.success('更新成功') - except Exception as e: - return Result.fail(e) + """更新电影分享链接 content-type为application/x-www-form-urlencoded + + Returns: + _type_: 更新结果 + """ + # 获取请求参数tmdb_id和share_link + movie_id = request.form.get('movie_id') + share_link = request.form.get('share_link') + if not movie_id or not share_link: + return Result.fail('movie_id和share_link不能为空') + try: + movie_handler.update_share_link(movie_id, share_link) + return Result.success('更新成功') + except Exception as e: + return Result.fail(e) # ==============================剧集=================================== +@blueprint.route('/show') +def shows(): + """分页获取剧集列表 + + Returns: + _type_: 剧集列表 + """ + # 获取请求参数page和page_size + curr_page = int(request.args.get('page', 1)) + page_size = int(request.args.get('page_size', 10)) + try: + return Result.success(show_handler.get_shows(curr_page, page_size)) + except Exception as e: + return Result.fail(e) + + +@blueprint.route('/show/') +def show(show_id): + """获取剧集详情 + + Args: + show_id (str): 剧集的tmdb_id + + Returns: + _type_: 剧集详情 + """ + try: + return Result.success(show_handler.get_show(show_id)) + except Exception as e: + return Result.fail(e) + + +@blueprint.route('/update_show_share_link', methods=['POST']) +def update_show_share_link(): + """更新剧集分享链接 content-type为application/x-www-form-urlencoded + + Returns: + _type_: 更新结果 + """ + # 获取请求参数tmdb_id和share_link + show_id = request.form.get('show_id') + share_link = request.form.get('share_link') + if not show_id or not share_link: + return Result.fail('show_id和share_link不能为空') + try: + show_handler.update_share_link(show_id, share_link) + return Result.success('更新成功') + except Exception as e: + return Result.fail(e) + + +# ==============================common================================ + + @blueprint.route('/index') def index(): - """获取电影/剧集索引 - """ - try: - return Result.success({ - 'movie': movie_handler.get_index(), - 'show': show_handler.get_index() - }) - except Exception as e: - return Result.fail(e) + """获取电影/剧集索引 + """ + try: + return Result.success({ + 'movie': movie_handler.get_index(), + 'show': show_handler.get_index() + }) + except Exception as e: + return Result.fail(e) @blueprint.route('/refresh_movie_cache') def refresh_movie_cache(): - """刷新电影缓存 - """ - try: - movie_handler.get_movies.cache_clear() - movie_handler.get_movie.cache_clear() - movie_handler.get_index.cache_clear() - return Result.success(msg='刷新movie缓存成功') - except Exception as e: - return Result.fail(e) + """刷新电影缓存 + """ + try: + movie_handler.get_movies.cache_clear() + movie_handler.get_movie.cache_clear() + movie_handler.get_index.cache_clear() + return Result.success(msg='刷新movie缓存成功') + except Exception as e: + return Result.fail(e) @blueprint.route('/refresh_show_cache') def refresh_show_cache(): - """刷新剧集缓存 - """ - try: - show_handler.get_index.cache_clear() - return Result.success(msg='刷新show缓存成功') - except Exception as e: - return Result.fail(e) + """刷新剧集缓存 + """ + try: + show_handler.get_index.cache_clear() + return Result.success(msg='刷新show缓存成功') + except Exception as e: + return Result.fail(e) diff --git a/components/trakt/movie_handler.py b/components/trakt/movie_handler.py index 244163b..e41b326 100644 --- a/components/trakt/movie_handler.py +++ b/components/trakt/movie_handler.py @@ -17,87 +17,87 @@ @cache.cache_with_expiry(876000) def get_movies(curr_page: int = 1, page_size: int = 10): - """分页查询电影列表 - - Args: - curr_page (int, optional): 第几页. 默认1 - - page_size (int, optional): 每页多少条数据. 默认10 - - Returns: - _type_: 电影列表 - """ - with acquire_client_sync() as turso_client: - page = Page(curr_page, page_size) - - # 查询总数 - total_count: int = turso_client.execute( - TOTAL_COUNT).rows[0][0] # type: ignore - # 设置总页数与总数 - page.set_total_count(total_count) - if total_count == 0: - return page - - page_data = [] - # 执行查询 - result_set = turso_client.execute(QUERY_MOVIE_BY_PAGED, { - 'page_size': page_size, 'offset': (curr_page - 1) * page_size}) - columns = result_set.columns - for row in result_set.rows: - data = {} - for index in range(0, len(columns)): - # 将查询结果转换为字典 - data[columns[index]] = row[index] - page_data.append(data) - page.data = page_data - return page + """分页查询电影列表 + + Args: + curr_page (int, optional): 第几页. 默认1 + + page_size (int, optional): 每页多少条数据. 默认10 + + Returns: + _type_: 电影列表 + """ + with acquire_client_sync() as turso_client: + page = Page(curr_page, page_size) + + # 查询总数 + total_count: int = turso_client.execute( + TOTAL_COUNT).rows[0][0] # type: ignore + # 设置总页数与总数 + page.set_total_count(total_count) + if total_count == 0: + return page + + page_data = [] + # 执行查询 + result_set = turso_client.execute(QUERY_MOVIE_BY_PAGED, { + 'page_size': page_size, 'offset': (curr_page - 1) * page_size}) + columns = result_set.columns + for row in result_set.rows: + data = {} + for index in range(0, len(columns)): + # 将查询结果转换为字典 + data[columns[index]] = row[index] + page_data.append(data) + page.data = page_data + return page @cache.cache_with_expiry(876000) def get_movie(movie_id: str): - """获取电影详情 + """获取电影详情 - Args: - movie_id (str): 电影的tmdb_id + Args: + movie_id (str): 电影的tmdb_id - Returns: - _type_: 电影详情 - """ - with acquire_client_sync() as turso_client: - # 查询电影详情 - rows = turso_client.execute( - SELECT_MOVIE_BY_ID, [movie_id]).rows - if len(rows) == 0: - return None - return rows[0] + Returns: + _type_: 电影详情 + """ + with acquire_client_sync() as turso_client: + # 查询电影详情 + rows = turso_client.execute( + SELECT_MOVIE_BY_ID, [movie_id]).rows + if len(rows) == 0: + return None + return rows[0] def update_share_link(movie_id: str, share_link: str): - """更新分享链接 + """更新分享链接 - Args: - movie_id (int): 电影的tmdb_id - share_link (str): 分享链接 - """ - with acquire_client_sync() as turso_client: - turso_client.execute(UPDATE_MOVIE_SHARE_LINK, { - 'movie_id': movie_id, 'share_link': share_link}) - # 清除缓存 - get_movies.cache_clear() - get_movie.cache_clear() + Args: + movie_id (int): 电影的tmdb_id + share_link (str): 分享链接 + """ + with acquire_client_sync() as turso_client: + turso_client.execute(UPDATE_MOVIE_SHARE_LINK, { + 'movie_id': movie_id, 'share_link': share_link}) + # 清除缓存 + get_movies.cache_clear() + get_movie.cache_clear() @cache.cache_with_expiry(876000) def get_index(): - """获取电影索引 - - Returns: - _type_: 索引数据(base64编码) - """ - with acquire_client_sync() as turso_client: - # 查询电影索引 - rows = turso_client.execute( - SELECT_LOCAL_SEARCH_BY_TYPE, ('movie',)).rows - if len(rows) == 0: - return '' - return rows[0]['b64_index'] + """获取电影索引 + + Returns: + _type_: 索引数据(base64编码) + """ + with acquire_client_sync() as turso_client: + # 查询电影索引 + rows = turso_client.execute( + SELECT_LOCAL_SEARCH_BY_TYPE, ('movie',)).rows + if len(rows) == 0: + return '' + return rows[0]['b64_index'] diff --git a/components/trakt/show_handler.py b/components/trakt/show_handler.py index ff85842..dd0ed33 100644 --- a/components/trakt/show_handler.py +++ b/components/trakt/show_handler.py @@ -1,7 +1,103 @@ # 剧集 处理类 from core import cache +from core.result import Page +from db.turso import acquire_client_sync + +# 查询总数 +TOTAL_COUNT = 'SELECT COUNT(*) FROM show' +# 分页查询剧集 列表(按照最后观看时间排序) +QUERY_SHOW_BY_PAGED = 'SELECT * FROM show ORDER BY last_watched_at DESC LIMIT :page_size OFFSET :offset' +# 更新分享链接 +UPDATE_SHOW_SHARE_LINK = 'UPDATE show SET share_link = :share_link WHERE show_id = :show_id' +# 根据类型查询索引表数据 +SELECT_LOCAL_SEARCH_BY_TYPE = "SELECT * FROM local_search WHERE type = ?" +# 根据show_id查询剧集详情 +SELECT_SHOW_BY_ID = "SELECT * FROM show WHERE show_id = ?" + + +@cache.cache_with_expiry(876000) +def get_shows(curr_page: int = 1, page_size: int = 10): + """分页查询剧集列表 + + Args: + curr_page (int, optional): 第几页. 默认1 + + page_size (int, optional): 每页多少条数据. 默认10 + + Returns: + _type_: 剧集列表 + """ + with acquire_client_sync() as turso_client: + page = Page(curr_page, page_size) + + # 查询总数 + total_count: int = turso_client.execute( + TOTAL_COUNT).rows[0][0] # type: ignore + # 设置总页数与总数 + page.set_total_count(total_count) + if total_count == 0: + return page + + page_data = [] + # 执行查询 + result_set = turso_client.execute(QUERY_SHOW_BY_PAGED, { + 'page_size': page_size, 'offset': (curr_page - 1) * page_size}) + columns = result_set.columns + for row in result_set.rows: + data = {} + for index in range(0, len(columns)): + # 将查询结果转换为字典 + data[columns[index]] = row[index] + page_data.append(data) + page.data = page_data + return page + + +@cache.cache_with_expiry(876000) +def get_show(show_id: str): + """获取剧集详情 + + Args: + show_id (str): 剧集的tmdb_id + + Returns: + _type_: 剧集详情 + """ + with acquire_client_sync() as turso_client: + # 查询电影详情 + rows = turso_client.execute( + SELECT_SHOW_BY_ID, [show_id]).rows + if len(rows) == 0: + return None + return rows[0] + + +def update_share_link(show_id: str, share_link: str): + """更新分享链接 + + Args: + show_id (int): 剧集的tmdb_id + share_link (str): 分享链接 + """ + with acquire_client_sync() as turso_client: + turso_client.execute(UPDATE_SHOW_SHARE_LINK, { + 'show_id': show_id, 'share_link': share_link}) + # 清除缓存 + get_shows.cache_clear() + get_show.cache_clear() @cache.cache_with_expiry(876000) def get_index(): - return '' + """获取剧集索引 + + Returns: + _type_: 索引数据(base64编码) + """ + with acquire_client_sync() as turso_client: + # 查询剧集索引 + rows = turso_client.execute( + SELECT_LOCAL_SEARCH_BY_TYPE, ('show',)).rows + if len(rows) == 0: + return '' + return rows[0]['b64_index'] diff --git a/core/result.py b/core/result.py index 574b780..f899306 100644 --- a/core/result.py +++ b/core/result.py @@ -1,84 +1,87 @@ import json from libsql_client.result import Row from typing import Union -# 统一结果类 - -class Page: - def __init__(self, curr_page: int = 1, page_size: int = 10, data=None): - """分页对象 +# 统一结果类 - Args: - data (_type_): 分页数据 - page (int, optional): 当前页 - page_size (int, optional): 每页多少条数据 - total_page (int, optional): 总页数 - total_count (int, optional): 总条数 - """ - self.curr_page = curr_page - self.page_size = page_size - self.data = data - def set_total_count(self, total_count: int): - """设置总页数与总数 +class Page: + + def __init__(self, curr_page: int = 1, page_size: int = 10, data=None): + """分页对象 - Args: - total_count (int): 总条数 - """ - self.total_count = total_count - self.total_page = total_count // self.page_size + \ - (1 if total_count % self.page_size > 0 else 0) + Args: + data (_type_): 分页数据 + page (int, optional): 当前页 + page_size (int, optional): 每页多少条数据 + total_page (int, optional): 总页数 + total_count (int, optional): 总条数 + """ + self.curr_page = curr_page + self.page_size = page_size + self.data = data + + def set_total_count(self, total_count: int): + """设置总页数与总数 - def __json__(self): - return { - 'curr_page': self.curr_page, - 'page_size': self.page_size, - 'total_page': self.total_page, - 'total_count': self.total_count, - 'data': self.data - } + Args: + total_count (int): 总条数 + """ + self.total_count = total_count + self.total_page = total_count // self.page_size + \ + (1 if total_count % self.page_size > 0 else 0) + + def __json__(self): + return { + 'curr_page': self.curr_page, + 'page_size': self.page_size, + 'total_page': self.total_page, + 'total_count': self.total_count, + 'data': self.data + } class Result: + + @staticmethod + def success(data: Union[str, dict, list, Page, Row, None] = None, code: int = 200, msg: str = 'success') -> str: + """成功响应 - @staticmethod - def success(data: Union[str, dict, list, Page, Row, None] = None, code: int = 200, msg: str = 'success') -> str: - """成功响应 - - Args: - data (Union[str, dict, list, Page, None], optional): _description_. Defaults to None. - code (int, optional): _description_. Defaults to 200. - msg (str, optional): _description_. Defaults to 'success'. - - Returns: - str: _description_ - """ - res = {} - if isinstance(data, Row): - res['data'] = data.asdict() - elif isinstance(data, Page): - res['data'] = data.__json__() - else: - res['data'] = data - res['code'] = code - res['msg'] = msg - return json.dumps(res) + Args: + data (Union[str, dict, list, Page, None], optional): _description_. Defaults to None. + code (int, optional): _description_. Defaults to 200. + msg (str, optional): _description_. Defaults to 'success'. - @staticmethod - def fail(msg: Union[str, Exception] = 'failed', code: int = 500, data: Union[str, dict, list, Page, None] = None) -> str: - """失败响应 + Returns: + str: _description_ + """ + res = {} + if isinstance(data, Row): + res['data'] = data.asdict() + elif isinstance(data, Page): + res['data'] = data.__json__() + else: + res['data'] = data + res['code'] = code + res['msg'] = msg + return json.dumps(res) + + @staticmethod + def fail(msg: Union[str, Exception] = 'failed', code: int = 500, + data: Union[str, dict, list, Page, None] = None) -> str: + """失败响应 - Args: - data (Union[str, dict, list, Page, None], optional): _description_. Defaults to None. - code (int, optional): _description_. Defaults to 500. - msg (str, optional): _description_. Defaults to 'failed'. + Args: + data (Union[str, dict, list, Page, None], optional): _description_. Defaults to None. + code (int, optional): _description_. Defaults to 500. + msg (str, optional): _description_. Defaults to 'failed'. - Returns: - str: _description_ - """ - res = {} - res['data'] = data.__json__() if isinstance(data, Page) else data - res['code'] = code - res['msg'] = msg if isinstance(msg, str) else str(msg) - return json.dumps(res) + Returns: + str: _description_ + """ + res = {} + res['data'] = data.__json__() if isinstance(data, Page) else data + res['code'] = code + res['msg'] = msg if isinstance(msg, str) else str(msg) + return json.dumps(res) diff --git a/utils/github.py b/utils/github.py index c517b1f..cb02368 100644 --- a/utils/github.py +++ b/utils/github.py @@ -4,19 +4,20 @@ GH_TOKEN = os.environ.get('GH_TOKEN') + def trigger_github_workflow(event_type: str, client_payload: dict = {}): - """触发github workflow + """触发github workflow - Args: - repo (str): 仓库名 - event_type (str): 事件类型 - client_payload (dict): 负载 - """ - header = { - 'Accept': 'application/vnd.github.everest-preview+json', - 'Authorization': f'token {GH_TOKEN}' - } - data = json.dumps({"event_type": f"{event_type}", - "client_payload": client_payload}) - requests.post(f'https://api.github.com/repos/nichuanfang/api/dispatches', - data=data, headers=header) \ No newline at end of file + Args: + repo (str): 仓库名 + event_type (str): 事件类型 + client_payload (dict): 负载 + """ + header = { + 'Accept': 'application/vnd.github.everest-preview+json', + 'Authorization': f'token {GH_TOKEN}' + } + data = json.dumps({"event_type": f"{event_type}", + "client_payload": client_payload}) + requests.post(f'https://api.github.com/repos/nichuanfang/api/dispatches', + data=data, headers=header)