diff --git a/m3u8_download.py b/m3u8_download.py index bc20aa9..2d278e1 100644 --- a/m3u8_download.py +++ b/m3u8_download.py @@ -1,8 +1,8 @@ -import m3u8_To_MP4 +import m3u8_to_mp4 from utils.MySqlUtil import MySqlUtil from apscheduler.schedulers.blocking import BlockingScheduler import time -from utils.log import Log +from utils.Log import Log from pathlib import Path @@ -30,7 +30,7 @@ def download_m3u8(download_path='./mp4/'): log.info(f"任务下载中,正在下载 {name}...") # 下载 m3u8 文件并转换为 MP4 - m3u8_To_MP4.multithread_download(url, file_path=str(file_path)) + m3u8_to_mp4.multithread_download(url, file_path=str(file_path)) log.info(f"成功下载并转换 {name} to {file_path}.") except Exception as e: diff --git a/m3u8_To_MP4/__init__.py b/m3u8_to_mp4/__init__.py similarity index 84% rename from m3u8_To_MP4/__init__.py rename to m3u8_to_mp4/__init__.py index 26f9d0d..3bebd12 100644 --- a/m3u8_To_MP4/__init__.py +++ b/m3u8_to_mp4/__init__.py @@ -15,7 +15,7 @@ m3u8_to_mp4.download("https://xxx.com/xxx/index.m3u8") import logging import subprocess -from m3u8_To_MP4.helpers import printer_helper +from m3u8_to_mp4.helpers import printer_helper printer_helper.config_logging() @@ -34,11 +34,11 @@ def verify_ffmpey(): # define API -import m3u8_To_MP4.multithreads_processor -from m3u8_To_MP4.v2_async_processor import AsynchronousFileCrawler -from m3u8_To_MP4.v2_async_processor import AsynchronousUriCrawler -from m3u8_To_MP4.v2_multithreads_processor import MultiThreadsFileCrawler -from m3u8_To_MP4.v2_multithreads_processor import MultiThreadsUriCrawler +import m3u8_to_mp4.multithreads_processor +from m3u8_to_mp4.v2_async_processor import AsynchronousFileCrawler +from m3u8_to_mp4.v2_async_processor import AsynchronousUriCrawler +from m3u8_to_mp4.v2_multithreads_processor import MultiThreadsFileCrawler +from m3u8_to_mp4.v2_multithreads_processor import MultiThreadsUriCrawler __all__ = ( "MultiThreadsFileCrawler", @@ -56,7 +56,7 @@ __all__ = ( # ================ Async =================== -def async_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http_header=None, max_retry_times=3, +def async_download(m3u8_uri, file_path='./m3u8_to_mp4.ts', customized_http_header=None, max_retry_times=3, num_concurrent=50, tmpdir=None): ''' Download mp4 video from given m3u uri. @@ -69,7 +69,7 @@ def async_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http_heade :return: ''' - with m3u8_To_MP4.v2_async_processor.AsynchronousUriCrawler(m3u8_uri, + with m3u8_to_mp4.v2_async_processor.AsynchronousUriCrawler(m3u8_uri, file_path, customized_http_header, max_retry_times, @@ -78,9 +78,9 @@ def async_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http_heade crawler.fetch_mp4_by_m3u8_uri('ts') -def async_uri_download(m3u8_uri, file_path='./m3u8_To_MP4.mp4', customized_http_header=None, +def async_uri_download(m3u8_uri, file_path='./m3u8_to_mp4.mp4', customized_http_header=None, max_retry_times=3, num_concurrent=50, tmpdir=None): - with m3u8_To_MP4.v2_async_processor.AsynchronousUriCrawler(m3u8_uri, + with m3u8_to_mp4.v2_async_processor.AsynchronousUriCrawler(m3u8_uri, file_path, customized_http_header, max_retry_times, @@ -89,9 +89,9 @@ def async_uri_download(m3u8_uri, file_path='./m3u8_To_MP4.mp4', customized_http_ crawler.fetch_mp4_by_m3u8_uri('ts') -def async_file_download(m3u8_uri, m3u8_file_path, file_path='./m3u8_To_MP4.ts', customized_http_header=None, +def async_file_download(m3u8_uri, m3u8_file_path, file_path='./m3u8_to_mp4.ts', customized_http_header=None, max_retry_times=3, num_concurrent=50, tmpdir=None): - with m3u8_To_MP4.v2_async_processor.AsynchronousFileCrawler(m3u8_uri, + with m3u8_to_mp4.v2_async_processor.AsynchronousFileCrawler(m3u8_uri, m3u8_file_path, file_path, customized_http_header, @@ -102,7 +102,7 @@ def async_file_download(m3u8_uri, m3u8_file_path, file_path='./m3u8_To_MP4.ts', # ================ MultiThread =================== -def multithread_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http_header=None, +def multithread_download(m3u8_uri, file_path='./m3u8_to_mp4.ts', customized_http_header=None, max_retry_times=3, max_num_workers=100, tmpdir=None): ''' Download mp4 video from given m3u uri. @@ -114,7 +114,7 @@ def multithread_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http :param mp4_file_name: a mp4 file name with suffix ".mp4" :return: ''' - with m3u8_To_MP4.v2_multithreads_processor.MultiThreadsUriCrawler(m3u8_uri, + with m3u8_to_mp4.v2_multithreads_processor.MultiThreadsUriCrawler(m3u8_uri, file_path, customized_http_header, max_retry_times, @@ -123,9 +123,9 @@ def multithread_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customized_http crawler.fetch_mp4_by_m3u8_uri('ts') -def multithread_uri_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customied_http_header=None, +def multithread_uri_download(m3u8_uri, file_path='./m3u8_to_mp4.ts', customied_http_header=None, max_retry_times=3, max_num_workers=100, tmpdir=None): - with m3u8_To_MP4.v2_multithreads_processor.MultiThreadsUriCrawler(m3u8_uri, + with m3u8_to_mp4.v2_multithreads_processor.MultiThreadsUriCrawler(m3u8_uri, file_path, customied_http_header, max_retry_times, @@ -137,7 +137,7 @@ def multithread_uri_download(m3u8_uri, file_path='./m3u8_To_MP4.ts', customied_h def multithread_file_download(m3u8_uri, m3u8_file_path, file_path, customized_http_header=None, max_retry_times=3, max_num_workers=100, tmpdir=None): - with m3u8_To_MP4.v2_multithreads_processor.MultiThreadsFileCrawler( + with m3u8_to_mp4.v2_multithreads_processor.MultiThreadsFileCrawler( m3u8_uri, m3u8_file_path, file_path, customized_http_header, max_retry_times, max_num_workers, tmpdir) as crawler: crawler.fetch_mp4_by_m3u8_uri(True) @@ -148,7 +148,7 @@ import warnings def download(m3u8_uri, max_retry_times=3, max_num_workers=100, - mp4_file_dir='./', mp4_file_name='m3u8_To_MP4', tmpdir=None): + mp4_file_dir='./', mp4_file_name='m3u8_to_mp4', tmpdir=None): ''' Download mp4 video from given m3u uri. @@ -163,7 +163,7 @@ def download(m3u8_uri, max_retry_times=3, max_num_workers=100, 'download function is deprecated, and please use multithread_download.', DeprecationWarning) - with m3u8_To_MP4.multithreads_processor.Crawler(m3u8_uri, max_retry_times, + with m3u8_to_mp4.multithreads_processor.Crawler(m3u8_uri, max_retry_times, max_num_workers, mp4_file_dir, mp4_file_name, diff --git a/m3u8_To_MP4/async_processor.py b/m3u8_to_mp4/async_processor.py similarity index 97% rename from m3u8_To_MP4/async_processor.py rename to m3u8_to_mp4/async_processor.py index bb45f6e..badae84 100644 --- a/m3u8_To_MP4/async_processor.py +++ b/m3u8_to_mp4/async_processor.py @@ -11,11 +11,11 @@ import zlib import m3u8 -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.helpers import printer_helper -from m3u8_To_MP4.networks.asynchronous import async_producer_consumer -from m3u8_To_MP4.networks.synchronous import sync_DNS -from m3u8_To_MP4.networks.synchronous import sync_http +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.helpers import printer_helper +from m3u8_to_mp4.networks.asynchronous import async_producer_consumer +from m3u8_to_mp4.networks.synchronous import sync_DNS +from m3u8_to_mp4.networks.synchronous import sync_http printer_helper.config_logging() diff --git a/m3u8_To_MP4/helpers/__init__.py b/m3u8_to_mp4/helpers/__init__.py similarity index 100% rename from m3u8_To_MP4/helpers/__init__.py rename to m3u8_to_mp4/helpers/__init__.py diff --git a/m3u8_To_MP4/helpers/os_helper.py b/m3u8_to_mp4/helpers/os_helper.py similarity index 100% rename from m3u8_To_MP4/helpers/os_helper.py rename to m3u8_to_mp4/helpers/os_helper.py diff --git a/m3u8_To_MP4/helpers/path_helper.py b/m3u8_to_mp4/helpers/path_helper.py similarity index 96% rename from m3u8_To_MP4/helpers/path_helper.py rename to m3u8_to_mp4/helpers/path_helper.py index 69eb81d..101e793 100644 --- a/m3u8_To_MP4/helpers/path_helper.py +++ b/m3u8_to_mp4/helpers/path_helper.py @@ -32,7 +32,7 @@ def random_5_char(): def random_name(): dt_str = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S') - return 'm3u8_To_MP4' + dt_str + random_5_char()+'.mp4' + return 'm3u8_to_mp4' + dt_str + random_5_char()+'.mp4' def calibrate_name(name): diff --git a/m3u8_To_MP4/helpers/printer_helper.py b/m3u8_to_mp4/helpers/printer_helper.py similarity index 100% rename from m3u8_To_MP4/helpers/printer_helper.py rename to m3u8_to_mp4/helpers/printer_helper.py diff --git a/m3u8_To_MP4/multithreads_processor.py b/m3u8_to_mp4/multithreads_processor.py similarity index 98% rename from m3u8_To_MP4/multithreads_processor.py rename to m3u8_to_mp4/multithreads_processor.py index 9a2c10b..e2502d2 100644 --- a/m3u8_To_MP4/multithreads_processor.py +++ b/m3u8_to_mp4/multithreads_processor.py @@ -12,9 +12,9 @@ import time import m3u8 from Crypto.Cipher import AES -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.helpers import printer_helper -from m3u8_To_MP4.networks.synchronous.sync_http_requester import request_for +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.helpers import printer_helper +from m3u8_to_mp4.networks.synchronous.sync_http_requester import request_for printer_helper.config_logging() diff --git a/m3u8_To_MP4/networks/__init__.py b/m3u8_to_mp4/networks/__init__.py similarity index 100% rename from m3u8_To_MP4/networks/__init__.py rename to m3u8_to_mp4/networks/__init__.py diff --git a/m3u8_To_MP4/networks/asynchronous/__init__.py b/m3u8_to_mp4/networks/asynchronous/__init__.py similarity index 100% rename from m3u8_To_MP4/networks/asynchronous/__init__.py rename to m3u8_to_mp4/networks/asynchronous/__init__.py diff --git a/m3u8_To_MP4/networks/asynchronous/async_DNS.py b/m3u8_to_mp4/networks/asynchronous/async_DNS.py similarity index 92% rename from m3u8_To_MP4/networks/asynchronous/async_DNS.py rename to m3u8_to_mp4/networks/asynchronous/async_DNS.py index fe560d1..d64e521 100644 --- a/m3u8_To_MP4/networks/asynchronous/async_DNS.py +++ b/m3u8_to_mp4/networks/asynchronous/async_DNS.py @@ -3,7 +3,7 @@ import asyncio import socket import urllib.parse -from m3u8_To_MP4.networks.http_base import AddressInfo +from m3u8_to_mp4.networks.http_base import AddressInfo async def available_addr_infos_of_url(url): diff --git a/m3u8_To_MP4/networks/asynchronous/async_http.py b/m3u8_to_mp4/networks/asynchronous/async_http.py similarity index 98% rename from m3u8_To_MP4/networks/asynchronous/async_http.py rename to m3u8_to_mp4/networks/asynchronous/async_http.py index b39903f..f007306 100644 --- a/m3u8_To_MP4/networks/asynchronous/async_http.py +++ b/m3u8_to_mp4/networks/asynchronous/async_http.py @@ -6,8 +6,8 @@ import urllib.parse import urllib.request import urllib.response -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.networks import http_base +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.networks import http_base def http_get_header(domain_name, port, resource_path_at_server, is_keep_alive): diff --git a/m3u8_To_MP4/networks/asynchronous/async_producer_consumer.py b/m3u8_to_mp4/networks/asynchronous/async_producer_consumer.py similarity index 96% rename from m3u8_To_MP4/networks/asynchronous/async_producer_consumer.py rename to m3u8_to_mp4/networks/asynchronous/async_producer_consumer.py index ba6c725..f97e137 100644 --- a/m3u8_To_MP4/networks/asynchronous/async_producer_consumer.py +++ b/m3u8_to_mp4/networks/asynchronous/async_producer_consumer.py @@ -12,10 +12,10 @@ from multiprocessing import JoinableQueue, Process from Crypto.Cipher import AES -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.helpers import printer_helper -from m3u8_To_MP4.networks import http_base -from m3u8_To_MP4.networks.asynchronous import async_http +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.helpers import printer_helper +from m3u8_to_mp4.networks import http_base +from m3u8_to_mp4.networks.asynchronous import async_http async def ts_request(concurrent_condition, ssl_context, addr_info, diff --git a/m3u8_To_MP4/networks/http_base.py b/m3u8_to_mp4/networks/http_base.py similarity index 100% rename from m3u8_To_MP4/networks/http_base.py rename to m3u8_to_mp4/networks/http_base.py diff --git a/m3u8_To_MP4/networks/synchronous/__init__.py b/m3u8_to_mp4/networks/synchronous/__init__.py similarity index 100% rename from m3u8_To_MP4/networks/synchronous/__init__.py rename to m3u8_to_mp4/networks/synchronous/__init__.py diff --git a/m3u8_To_MP4/networks/synchronous/sync_DNS.py b/m3u8_to_mp4/networks/synchronous/sync_DNS.py similarity index 94% rename from m3u8_To_MP4/networks/synchronous/sync_DNS.py rename to m3u8_to_mp4/networks/synchronous/sync_DNS.py index fb5a27b..acaa929 100644 --- a/m3u8_To_MP4/networks/synchronous/sync_DNS.py +++ b/m3u8_to_mp4/networks/synchronous/sync_DNS.py @@ -3,7 +3,7 @@ import re import socket import urllib.parse -from m3u8_To_MP4.networks.http_base import AddressInfo +from m3u8_to_mp4.networks.http_base import AddressInfo def available_addr_infos_of_url(url): diff --git a/m3u8_To_MP4/networks/synchronous/sync_http.py b/m3u8_to_mp4/networks/synchronous/sync_http.py similarity index 96% rename from m3u8_To_MP4/networks/synchronous/sync_http.py rename to m3u8_to_mp4/networks/synchronous/sync_http.py index e9f3dd5..80de090 100644 --- a/m3u8_To_MP4/networks/synchronous/sync_http.py +++ b/m3u8_to_mp4/networks/synchronous/sync_http.py @@ -4,8 +4,8 @@ import urllib.parse import urllib.request import urllib.response -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.networks import http_base +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.networks import http_base def http_get_header(domain_name, port, resource_path_at_server, is_keep_alive, diff --git a/m3u8_To_MP4/networks/synchronous/sync_http_requester.py b/m3u8_to_mp4/networks/synchronous/sync_http_requester.py similarity index 100% rename from m3u8_To_MP4/networks/synchronous/sync_http_requester.py rename to m3u8_to_mp4/networks/synchronous/sync_http_requester.py diff --git a/m3u8_To_MP4/v2_abstract_crawler_processor.py b/m3u8_to_mp4/v2_abstract_crawler_processor.py similarity index 97% rename from m3u8_To_MP4/v2_abstract_crawler_processor.py rename to m3u8_to_mp4/v2_abstract_crawler_processor.py index 25e8d1c..5f9d372 100644 --- a/m3u8_To_MP4/v2_abstract_crawler_processor.py +++ b/m3u8_to_mp4/v2_abstract_crawler_processor.py @@ -9,10 +9,10 @@ import time import warnings import zlib -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.helpers import printer_helper -from m3u8_To_MP4.helpers.os_helper import get_core_count -from m3u8_To_MP4.networks.synchronous import sync_DNS +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.helpers import printer_helper +from m3u8_to_mp4.helpers.os_helper import get_core_count +from m3u8_to_mp4.networks.synchronous import sync_DNS printer_helper.config_logging() @@ -20,7 +20,7 @@ printer_helper.config_logging() class AbstractCrawler(object): def __init__(self, m3u8_uri, - file_path='./m3u8_To_MP4.mp4', + file_path='./m3u8_to_mp4.mp4', customized_http_header=None, max_retry_times=3, num_concurrent=50, diff --git a/m3u8_To_MP4/v2_abstract_task_processor.py b/m3u8_to_mp4/v2_abstract_task_processor.py similarity index 98% rename from m3u8_To_MP4/v2_abstract_task_processor.py rename to m3u8_to_mp4/v2_abstract_task_processor.py index 2c18f9b..aec08fb 100644 --- a/m3u8_To_MP4/v2_abstract_task_processor.py +++ b/m3u8_to_mp4/v2_abstract_task_processor.py @@ -5,8 +5,8 @@ import os.path import m3u8 -from m3u8_To_MP4.networks.synchronous import sync_http -from m3u8_To_MP4.v2_abstract_crawler_processor import AbstractCrawler +from m3u8_to_mp4.networks.synchronous import sync_http +from m3u8_to_mp4.v2_abstract_crawler_processor import AbstractCrawler EncryptedKey = collections.namedtuple(typename='EncryptedKey', field_names=['method', 'value', 'iv']) diff --git a/m3u8_To_MP4/v2_async_processor.py b/m3u8_to_mp4/v2_async_processor.py similarity index 90% rename from m3u8_To_MP4/v2_async_processor.py rename to m3u8_to_mp4/v2_async_processor.py index ba94750..7d5306c 100644 --- a/m3u8_To_MP4/v2_async_processor.py +++ b/m3u8_to_mp4/v2_async_processor.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from m3u8_To_MP4 import v2_abstract_task_processor -from m3u8_To_MP4.networks.asynchronous import async_producer_consumer +from m3u8_to_mp4 import v2_abstract_task_processor +from m3u8_to_mp4.networks.asynchronous import async_producer_consumer class AsynchronousFileCrawler(v2_abstract_task_processor.AbstractFileCrawler): diff --git a/m3u8_To_MP4/v2_multithreads_processor.py b/m3u8_to_mp4/v2_multithreads_processor.py similarity index 96% rename from m3u8_To_MP4/v2_multithreads_processor.py rename to m3u8_to_mp4/v2_multithreads_processor.py index 9274530..93de83a 100644 --- a/m3u8_To_MP4/v2_multithreads_processor.py +++ b/m3u8_to_mp4/v2_multithreads_processor.py @@ -6,10 +6,10 @@ import sys from Crypto.Cipher import AES -from m3u8_To_MP4 import v2_abstract_task_processor -from m3u8_To_MP4.helpers import path_helper -from m3u8_To_MP4.helpers import printer_helper -from m3u8_To_MP4.networks.synchronous.sync_http_requester import request_for +from m3u8_to_mp4 import v2_abstract_task_processor +from m3u8_to_mp4.helpers import path_helper +from m3u8_to_mp4.helpers import printer_helper +from m3u8_to_mp4.networks.synchronous.sync_http_requester import request_for def download_segment(segment_url, customized_http_header): diff --git a/utils/log.py b/utils/Log.py similarity index 100% rename from utils/log.py rename to utils/Log.py diff --git a/utils/SQLiteDB.py b/utils/SQLiteDB.py new file mode 100644 index 0000000..f5f8a35 --- /dev/null +++ b/utils/SQLiteDB.py @@ -0,0 +1,125 @@ +import sqlite3 +from typing import Optional, List, Tuple, Any + + +class SQLiteDB: + def __init__(self, db_path: str = "./sqlite/movies.db"): + self.db_path = db_path + self.conn: Optional[sqlite3.Connection] = None + self.cursor: Optional[sqlite3.Cursor] = None + self._init_db() + + def _init_db(self) -> None: + """初始化数据库并创建表(基于网页4、网页5、网页7的表结构设计)[4,5,7](@ref)""" + create_table_sql = """ + CREATE TABLE IF NOT EXISTS movie ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + path TEXT NOT NULL, + is_download INTEGER DEFAULT 0 + ) + """ + self.connect() + if self.cursor: + self.cursor.execute(create_table_sql) + self.conn.commit() + + def connect(self) -> None: + """建立数据库连接(基于网页2、网页5的连接方式)[2,5](@ref)""" + try: + self.conn = sqlite3.connect(self.db_path) + self.cursor = self.ursor.cursor() + self.conn.execute("PRAGMA foreign_keys = ON") # 启用外键约束 + except sqlite3.Error as e: + raise ConnectionError(f"数据库连接失败: {e}") + + def close(self) -> None: + """安全关闭连接(参考网页6的资源管理)[6](@ref)""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() + + def execute( + self, + sql: str, + params: Optional[Tuple[Any, ...]] = None, + commit: bool = True + ) -> int: + """ + 执行单条SQL语句(集成网页1、网页7的参数化查询)[1,7](@ref) + :param sql: 含占位符的SQL语句 + :param params: 参数元组(防SQL注入) + :param commit: 是否自动提交事务 + :return: 影响的行数 + """ + try: + if params: + self.cursor.execute(sql, params) + else: + self.cursor.execute(sql) + if commit and self.conn: + self.conn.commit() + return self.cursor.rowcount + except sqlite3.Error as e: + if self.conn: + self.conn.rollback() + raise RuntimeError(f"SQL执行失败: {e}") + + # ---- 高级操作方法 ---- + def insert_movie(self, name: str, path: str) -> int: + """插入电影记录(演示特定业务方法)""" + return self.execute( + "INSERT INTO movie (name, path) VALUES (?, ?)", + (name, path) + ) + + def mark_downloaded(self, movie_id: int) -> int: + """标记电影为已下载(业务逻辑示例)[6](@ref)""" + return self.execute( + "UPDATE movie SET is_download = 1 WHERE id = ?", + (movie_id,) + ) + + def get_undownloaded(self) -> List[Tuple]: + """查询未下载的电影(返回完整记录元组)[4,6](@ref)""" + self.execute("SELECT * FROM movie WHERE is_download = 0", commit=False) + return self.cursor.fetchall() + + # ---- 上下文管理器支持 ---- + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +# ==================== 使用示例 ==================== +if __name__ == "__main__": + # 示例1:自动建表并插入数据 + with SQLiteDB() as db: + # 批量插入演示(集成网页5的executemany方法)[5](@ref) + movies = [("泰坦尼克号", "/movies/titanic"), ("阿凡达", "/movies/avatar")] + db.executemany( + "INSERT INTO movie (name, path) VALUES (?, ?)", + movies + ) + + # 查询未下载记录(基于网页6的查询模式)[6](@ref) + print("待下载电影:", db.get_undownloaded()) + # + # # 示例2:更新操作 + # with SQLiteDB() as db: + # db.mark_downloaded(1) + # print("更新后的记录:", db.get_undownloaded()) + # + # # 示例3:事务回滚演示 + # try: + # with SQLiteDB() as db: + # db.execute("BEGIN TRANSACTION", commit=False) + # db.insert_movie("黑客帝国", "/movies/matrix") + # raise RuntimeError("模拟业务异常") # 触发回滚 + # db.execute("COMMIT", commit=True) + # except Exception as e: + # print(f"事务回滚成功: {e}") \ No newline at end of file