├── .java-version ├── frontend ├── public │ ├── robots.txt │ └── favicon.ico ├── server │ └── tsconfig.json ├── tsconfig.json ├── .gitignore ├── plugins │ ├── auth.client.js │ ├── logger.client.js │ └── docker-port-fix.client.js ├── package.json ├── middleware │ ├── guest.js │ ├── docker-port.global.js │ └── auth.js ├── app.vue ├── tailwind.config.js ├── nuxt.config.ts ├── .trae.yml └── utils │ └── api.js ├── .vercel └── project.json ├── backend ├── settings.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradle.properties ├── src │ ├── main │ │ ├── resources │ │ │ ├── db │ │ │ │ └── migration │ │ │ │ │ ├── V1_0_6__add_strm_base_url_column.sql │ │ │ │ │ ├── V1_0_7__add_enable_url_encoding_column.sql │ │ │ │ │ ├── V1_0_0__init_schema.sql │ │ │ │ │ ├── V1_0_3__create_openlist_config_table.sql │ │ │ │ │ ├── V1_0_1__insert_urp_table.sql │ │ │ │ │ ├── V1_0_4__create_task_config_table.sql │ │ │ │ │ └── V1_0_5__modify_need_rename_to_rename_regex.sql │ │ │ ├── application-prod.yml │ │ │ └── application.yml │ │ └── java │ │ │ └── com │ │ │ └── hienao │ │ │ └── openlist2strm │ │ │ ├── dto │ │ │ ├── sign │ │ │ │ ├── SignUpDto.java │ │ │ │ ├── SignInDto.java │ │ │ │ └── ChangePasswordDto.java │ │ │ ├── PageResponseDto.java │ │ │ ├── version │ │ │ │ ├── GitHubAsset.java │ │ │ │ ├── VersionCheckResponse.java │ │ │ │ └── GitHubRelease.java │ │ │ ├── DataReportRequest.java │ │ │ ├── openlist │ │ │ │ └── OpenlistConfigDto.java │ │ │ ├── ApiResponse.java │ │ │ ├── task │ │ │ │ └── TaskConfigDto.java │ │ │ ├── FrontendLogRequest.java │ │ │ ├── media │ │ │ │ ├── AiRecognitionResult.java │ │ │ │ └── MediaInfo.java │ │ │ ├── tmdb │ │ │ │ └── TmdbSearchResponse.java │ │ │ └── PageRequestDto.java │ │ │ ├── config │ │ │ ├── PasswordEncoderConfig.java │ │ │ ├── security │ │ │ │ ├── HttpFireWallConfig.java │ │ │ │ ├── JwtAuthenticationToken.java │ │ │ │ ├── RestfulAuthenticationEntryPointHandler.java │ │ │ │ ├── UserDetailsServiceImpl.java │ │ │ │ ├── JwtAuthenticationFilter.java │ │ │ │ └── Jwt.java │ │ │ ├── TaskExecutorConfig.java │ │ │ ├── WebSocketConfig.java │ │ │ ├── cache │ │ │ │ └── CacheConfig.java │ │ │ ├── RestTemplateConfig.java │ │ │ ├── CorsConfig.java │ │ │ ├── OpenApiConfig.java │ │ │ ├── PathConfiguration.java │ │ │ └── PathConfigurationValidator.java │ │ │ ├── job │ │ │ ├── DataBackupJob.java │ │ │ ├── EmailJob.java │ │ │ ├── VersionCheckJob.java │ │ │ └── TaskConfigJob.java │ │ │ ├── exception │ │ │ ├── BusinessException.java │ │ │ └── GlobalExceptionHandler.java │ │ │ ├── validation │ │ │ ├── ValidCronExpression.java │ │ │ └── CronExpressionValidator.java │ │ │ ├── entity │ │ │ ├── OpenlistConfig.java │ │ │ └── TaskConfig.java │ │ │ ├── service │ │ │ ├── CacheService.java │ │ │ └── DataReportUsageExample.java │ │ │ ├── ApplicationService.java │ │ │ ├── mapper │ │ │ ├── OpenlistConfigMapper.java │ │ │ └── TaskConfigMapper.java │ │ │ ├── controller │ │ │ ├── VersionController.java │ │ │ └── DataReportController.java │ │ │ └── util │ │ │ └── UrlEncoder.java │ └── test │ │ └── java │ │ └── com │ │ └── hienao │ │ └── openlist2strm │ │ ├── util │ │ └── TmdbIdExtractorTest.java │ │ ├── service │ │ └── QuartzSchedulerServiceTest.java │ │ ├── integration │ │ └── cache │ │ │ └── CacheTest.java │ │ └── unit │ │ ├── JwtUnitTest.java │ │ └── PageRequestDtoUnitTest.java ├── pmd-rules.xml ├── LICENSE.txt └── gradlew.bat ├── screenshots ├── home.jpg ├── logs.jpg ├── add_task.jpg ├── task_manage.jpg └── add_openlist.jpg ├── .gitignore ├── .bmad-core ├── data │ ├── technical-preferences.md │ ├── brainstorming-techniques.md │ └── test-levels-framework.md ├── agent-teams │ ├── team-ide-minimal.yaml │ ├── team-no-ui.yaml │ ├── team-all.yaml │ └── team-fullstack.yaml ├── core-config.yaml ├── utils │ └── workflow-management.md ├── tasks │ ├── kb-mode-interaction.md │ └── create-doc.md └── templates │ └── qa-gate-tmpl.yaml ├── .kilocode └── rules │ ├── test.md │ └── memory-bank │ ├── product.md │ ├── brief.md │ └── context.md ├── package.json ├── docker-compose.yml ├── docs ├── index.md ├── log.md ├── .vitepress │ └── config.mts ├── faq.md ├── url-encoding-config.md ├── quick-start.md ├── system-config.md └── add-openlist.md ├── Caddyfile ├── .github ├── DOCKER_SETUP.md └── release-changelog-config.json ├── .env.docker.example ├── .dockerignore ├── nginx.conf └── README.md /.java-version: -------------------------------------------------------------------------------- 1 | 21 2 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /.vercel/project.json: -------------------------------------------------------------------------------- 1 | {"projectName":"trae_openlisttostrm_q8yp"} -------------------------------------------------------------------------------- /backend/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "openlisttostrm" 2 | -------------------------------------------------------------------------------- /screenshots/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/screenshots/home.jpg -------------------------------------------------------------------------------- /screenshots/logs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/screenshots/logs.jpg -------------------------------------------------------------------------------- /frontend/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /screenshots/add_task.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/screenshots/add_task.jpg -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/task_manage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/screenshots/task_manage.jpg -------------------------------------------------------------------------------- /screenshots/add_openlist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/screenshots/add_openlist.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | analog/ 3 | node_modules/ 4 | docs/.vitepress/dist 5 | docs/.vitepress/cache 6 | llmdoc/ 7 | .claude/ -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hienao/ostrm/HEAD/backend/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /.bmad-core/data/technical-preferences.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # User-Defined Preferred Patterns and Preferences 4 | 5 | None Listed 6 | -------------------------------------------------------------------------------- /.kilocode/rules/test.md: -------------------------------------------------------------------------------- 1 | # 关于开发过程中测试 2 | 测试时,始终使用docker一键脚本进行部署测试,在不同平台下使用脚本有差异: 3 | windows下使用:dev-docker-rebuild.bat 4 | mac或linux下使用:dev-docker-rebuild.sh -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 15 22:46:56 CST 2025 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 3 | -------------------------------------------------------------------------------- /backend/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.configuration-cache=true 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | org.gradle.daemon=true 5 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m 6 | org.gradle.configureondemand=true 7 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_6__add_strm_base_url_column.sql: -------------------------------------------------------------------------------- 1 | -- 添加strm_base_url字段用于STRM文件生成时的baseUrl替换 2 | ALTER TABLE openlist_config ADD COLUMN strm_base_url VARCHAR(500); 3 | 4 | -- 添加注释说明字段用途 5 | -- strm_base_url: 用于STRM文件生成时替换原始URL的baseUrl,可为空,为空时则不进行替换 -------------------------------------------------------------------------------- /.bmad-core/agent-teams/team-ide-minimal.yaml: -------------------------------------------------------------------------------- 1 | # 2 | bundle: 3 | name: Team IDE Minimal 4 | icon: ⚡ 5 | description: Only the bare minimum for the IDE PO SM dev qa cycle. 6 | agents: 7 | - po 8 | - sm 9 | - dev 10 | - qa 11 | workflows: null 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_7__add_enable_url_encoding_column.sql: -------------------------------------------------------------------------------- 1 | -- 添加enable_url_encoding字段用于控制STRM链接编码 2 | ALTER TABLE openlist_config ADD COLUMN enable_url_encoding TINYINT(1) DEFAULT 1; 3 | 4 | -- 添加注释说明字段用途 5 | -- enable_url_encoding: 控制是否对STRM文件中的链接进行URL编码,默认为1(启用编码) 6 | -- 1: 启用URL编码(默认行为,保持向后兼容) 7 | -- 0: 禁用URL编码,使用原始URL -------------------------------------------------------------------------------- /.bmad-core/agent-teams/team-no-ui.yaml: -------------------------------------------------------------------------------- 1 | # 2 | bundle: 3 | name: Team No UI 4 | icon: 🔧 5 | description: Team with no UX or UI Planning. 6 | agents: 7 | - bmad-orchestrator 8 | - analyst 9 | - pm 10 | - architect 11 | - po 12 | workflows: 13 | - greenfield-service.yaml 14 | - brownfield-service.yaml 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pinia/nuxt": "^0.11.2", 4 | "pinia": "^3.0.3" 5 | }, 6 | "devDependencies": { 7 | "vitepress": "^2.0.0-alpha.12" 8 | }, 9 | "scripts": { 10 | "docs:dev": "vitepress dev docs", 11 | "docs:build": "vitepress build docs", 12 | "docs:preview": "vitepress preview docs" 13 | } 14 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/sign/SignUpDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.sign; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import lombok.*; 5 | 6 | @Getter 7 | @Setter 8 | @ToString 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class SignUpDto { 12 | @NotEmpty private String username; 13 | 14 | @NotEmpty private String password; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/sign/SignInDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.sign; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import lombok.*; 5 | 6 | @Getter 7 | @Setter 8 | @ToString 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class SignInDto { 12 | 13 | @NotEmpty private String username; 14 | 15 | @NotEmpty private String password; 16 | } 17 | -------------------------------------------------------------------------------- /.bmad-core/agent-teams/team-all.yaml: -------------------------------------------------------------------------------- 1 | # 2 | bundle: 3 | name: Team All 4 | icon: 👥 5 | description: Includes every core system agent. 6 | agents: 7 | - bmad-orchestrator 8 | - "*" 9 | workflows: 10 | - brownfield-fullstack.yaml 11 | - brownfield-service.yaml 12 | - brownfield-ui.yaml 13 | - greenfield-fullstack.yaml 14 | - greenfield-service.yaml 15 | - greenfield-ui.yaml 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/sign/ChangePasswordDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.sign; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import lombok.*; 5 | 6 | @Getter 7 | @Setter 8 | @ToString 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class ChangePasswordDto { 12 | @NotEmpty private String oldPassword; 13 | 14 | @NotEmpty private String newPassword; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/plugins/auth.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 认证插件 - 客户端初始化 3 | * 在应用启动时恢复认证状态 4 | */ 5 | 6 | export default defineNuxtPlugin(async () => { 7 | // 在客户端启动时恢复认证状态 8 | const { useAuthStore } = await import('~/stores/auth.js') 9 | const logger = await import('~/utils/logger.js').then(m => m.default) 10 | const authStore = useAuthStore() 11 | 12 | // 恢复认证状态 13 | authStore.restoreAuth() 14 | 15 | logger.info('认证插件已初始化,当前认证状态:', authStore.isAuthenticated) 16 | }) 17 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_0__init_schema.sql: -------------------------------------------------------------------------------- 1 | -- 初始化数据库架构 2 | -- 这是一个占位符迁移文件,用于确保Flyway迁移序列的完整性 3 | -- 实际的用户认证使用基于文件的系统 (userInfo.json) 4 | 5 | -- 创建一个简单的系统信息表来记录数据库初始化 6 | CREATE TABLE IF NOT EXISTS system_info ( 7 | id INTEGER PRIMARY KEY AUTOINCREMENT, 8 | version VARCHAR(50) NOT NULL, 9 | initialized_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 10 | description TEXT 11 | ); 12 | 13 | -- 插入初始化记录 14 | INSERT INTO system_info (version, description) 15 | VALUES ('1.0.0', 'Database schema initialized'); -------------------------------------------------------------------------------- /.bmad-core/agent-teams/team-fullstack.yaml: -------------------------------------------------------------------------------- 1 | # 2 | bundle: 3 | name: Team Fullstack 4 | icon: 🚀 5 | description: Team capable of full stack, front end only, or service development. 6 | agents: 7 | - bmad-orchestrator 8 | - analyst 9 | - pm 10 | - ux-expert 11 | - architect 12 | - po 13 | workflows: 14 | - brownfield-fullstack.yaml 15 | - brownfield-service.yaml 16 | - brownfield-ui.yaml 17 | - greenfield-fullstack.yaml 18 | - greenfield-service.yaml 19 | - greenfield-ui.yaml 20 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/PasswordEncoderConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | @Configuration 9 | public class PasswordEncoderConfig { 10 | 11 | @Bean 12 | public PasswordEncoder passwordEncoder() { 13 | return new BCryptPasswordEncoder(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@nuxtjs/tailwindcss": "^6.12.1", 14 | "@pinia/nuxt": "^0.5.1", 15 | "@tailwindcss/forms": "^0.5.9", 16 | "nuxt": "^3.13.0", 17 | "pinia": "^2.1.7", 18 | "tailwindcss": "^3.4.15", 19 | "vue": "^3.4.0", 20 | "vue-router": "^4.4.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/PageResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto; 2 | 3 | import jakarta.annotation.Nullable; 4 | import lombok.*; 5 | 6 | @Data 7 | public class PageResponseDto { 8 | private long total; 9 | private T data; 10 | 11 | public PageResponseDto(long total, @Nullable T data) { 12 | if (total < 0) { 13 | throw new IllegalArgumentException("total must not be less than zero"); 14 | } 15 | this.total = total; 16 | this.data = data; 17 | } 18 | 19 | public static PageResponseDto empty() { 20 | return new PageResponseDto<>(0, null); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/job/DataBackupJob.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.job; 2 | 3 | import java.text.MessageFormat; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.quartz.Job; 6 | import org.quartz.JobExecutionContext; 7 | 8 | @Slf4j 9 | public class DataBackupJob implements Job { 10 | 11 | @Override 12 | public void execute(JobExecutionContext context) { 13 | String userId = context.getJobDetail().getJobDataMap().getString("userId"); 14 | log.info( 15 | MessageFormat.format( 16 | "Job execute: JobName {0} Param {1} Thread: {2}", 17 | getClass(), userId, Thread.currentThread().getName())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/job/EmailJob.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.job; 2 | 3 | import java.text.MessageFormat; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.quartz.Job; 6 | import org.quartz.JobExecutionContext; 7 | 8 | @Slf4j 9 | public class EmailJob implements Job { 10 | 11 | @Override 12 | public void execute(JobExecutionContext context) { 13 | String userEmail = context.getJobDetail().getJobDataMap().getString("userEmail"); 14 | log.info( 15 | MessageFormat.format( 16 | "Job execute: JobName {0} Param {1} Thread: {2}", 17 | getClass(), userEmail, Thread.currentThread().getName())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/plugins/logger.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 前端日志收集插件 3 | * 在客户端启动时初始化日志收集服务 4 | */ 5 | 6 | import logger from '~/utils/logger.js' 7 | 8 | export default defineNuxtPlugin(() => { 9 | // 只在客户端运行 10 | if (process.client) { 11 | // 初始化日志收集 12 | console.info('前端日志收集服务已启动') 13 | 14 | // 记录页面加载信息 15 | logger.info('页面加载完成', { 16 | url: window.location.href, 17 | referrer: document.referrer 18 | }) 19 | 20 | // 监听路由变化 21 | const router = useRouter() 22 | router.afterEach((to, from) => { 23 | logger.info(`路由变化: ${from.path} -> ${to.path}`, { 24 | from: from.path, 25 | to: to.path 26 | }) 27 | }) 28 | } 29 | }) -------------------------------------------------------------------------------- /.bmad-core/core-config.yaml: -------------------------------------------------------------------------------- 1 | # 2 | markdownExploder: true 3 | qa: 4 | qaLocation: docs/qa 5 | prd: 6 | prdFile: docs/prd.md 7 | prdVersion: v4 8 | prdSharded: true 9 | prdShardedLocation: docs/prd 10 | epicFilePattern: epic-{n}*.md 11 | architecture: 12 | architectureFile: docs/architecture.md 13 | architectureVersion: v4 14 | architectureSharded: true 15 | architectureShardedLocation: docs/architecture 16 | customTechnicalDocuments: null 17 | devLoadAlwaysFiles: 18 | - docs/architecture/coding-standards.md 19 | - docs/architecture/tech-stack.md 20 | - docs/architecture/source-tree.md 21 | devDebugLog: .ai/debug-log.md 22 | devStoryLocation: docs/stories 23 | slashPrefix: BMad 24 | -------------------------------------------------------------------------------- /frontend/middleware/guest.js: -------------------------------------------------------------------------------- 1 | // 访客中间件 - 只允许未登录用户访问(如登录页、注册页) 2 | 3 | export default defineNuxtRouteMiddleware(async (to, from) => { 4 | console.log('Guest中间件执行:', { to: to.path, from: from?.path }) 5 | 6 | // 获取认证store 7 | const { useAuthStore } = await import('~/stores/auth.js') 8 | const authStore = useAuthStore() 9 | 10 | // 尝试恢复认证状态 11 | authStore.restoreAuth() 12 | 13 | console.log('Guest中间件 - 认证状态:', { 14 | token: authStore.getToken, 15 | isAuthenticated: authStore.isAuthenticated 16 | }) 17 | 18 | // 如果已认证,跳转到首页 19 | if (authStore.isAuthenticated) { 20 | console.log('Guest中间件 - 检测到有效认证,准备跳转到首页') 21 | return navigateTo('/') 22 | } 23 | 24 | console.log('Guest中间件 - 未认证,允许访问当前页面') 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: ./Dockerfile 6 | container_name: app 7 | hostname: app 8 | environment: 9 | SPRING_PROFILES_ACTIVE: prod 10 | LOG_PATH: /maindata/log 11 | DATABASE_PATH: /maindata/db/openlist2strm.db 12 | CONFIG_PATH: /maindata/config 13 | USER_INFO_PATH: /maindata/config/userInfo.json 14 | FRONTEND_LOGS_PATH: /maindata/log/frontend 15 | ports: 16 | - "3111:80" # External port:Internal port, users can modify external port as needed 17 | volumes: 18 | - ${LOG_PATH_HOST}:/maindata/log 19 | - ${CONFIG_PATH_HOST}:/maindata/config 20 | - ${DB_PATH_HOST}:/maindata/db 21 | - ${STRM_PATH_HOST}:/app/backend/strm 22 | restart: always -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.exception; 2 | 3 | public class BusinessException extends RuntimeException { 4 | 5 | @java.io.Serial private static final long serialVersionUID = -2119302295305964305L; 6 | 7 | public BusinessException() {} 8 | 9 | public BusinessException(String message) { 10 | super(message); 11 | } 12 | 13 | public BusinessException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BusinessException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | public BusinessException( 22 | String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/version/GitHubAsset.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.version; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | /** 7 | * GitHub Release Asset 信息DTO 8 | * 9 | * @author hienao 10 | * @since 2024-01-01 11 | */ 12 | @Data 13 | public class GitHubAsset { 14 | 15 | private String id; 16 | private String name; 17 | private String label; 18 | private String contentType; 19 | 20 | private long size; 21 | 22 | @JsonProperty("download_count") 23 | private long downloadCount; 24 | 25 | @JsonProperty("created_at") 26 | private String createdAt; 27 | 28 | @JsonProperty("updated_at") 29 | private String updatedAt; 30 | 31 | @JsonProperty("browser_download_url") 32 | private String browserDownloadUrl; 33 | } 34 | -------------------------------------------------------------------------------- /backend/pmd-rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | This is a custom ruleset for our project. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/validation/ValidCronExpression.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.validation; 2 | 3 | import jakarta.validation.Constraint; 4 | import jakarta.validation.Payload; 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Cron表达式验证注解 12 | * 13 | * @author hienao 14 | * @since 2024-01-01 15 | */ 16 | @Target({ElementType.FIELD, ElementType.PARAMETER}) 17 | @Retention(RetentionPolicy.RUNTIME) 18 | @Constraint(validatedBy = CronExpressionValidator.class) 19 | public @interface ValidCronExpression { 20 | 21 | String message() default "定时任务表达式格式不正确"; 22 | 23 | Class[] groups() default {}; 24 | 25 | Class[] payload() default {}; 26 | } 27 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "OpenList-Strm" 7 | text: "便捷的为你的OpenList影音文件生成Strm文件" 8 | tagline: 一个OpenList的配套使用工具 9 | actions: 10 | - theme: brand 11 | text: 快速开始 12 | link: /quick-start 13 | - theme: alt 14 | text: GitHub 仓库 15 | link: https://github.com/hienao/ostrm 16 | 17 | features: 18 | - title: 🎬 STRM 文件生成 19 | details: 自动将 OpenList 文件列表转换为 STRM 流媒体文件,支持多种媒体格式 20 | - title: 📋 任务管理系统 21 | details: 完整的 Web 界面,支持任务的创建、编辑、删除和状态监控 22 | - title: ⏰ 定时任务调度 23 | details: 基于 Cron 表达式的自动化执行,支持增量和全量更新模式 24 | - title: 🔍 AI 智能刮削 25 | details: 可选的智能媒体信息刮削功能,自动获取电影和电视剧元数据 26 | - title: 🔐 安全认证系统 27 | details: 基于 JWT 的用户认证机制,确保数据安全 28 | - title: 🐳 容器化部署 29 | details: 完整的 Docker 支持,一键部署,支持环境变量配置 30 | --- 31 | 32 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/version/VersionCheckResponse.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.version; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.Data; 5 | 6 | /** 7 | * 版本检查响应DTO 8 | * 9 | * @author hienao 10 | * @since 2024-01-01 11 | */ 12 | @Data 13 | public class VersionCheckResponse { 14 | 15 | /** 当前版本 */ 16 | private String currentVersion; 17 | 18 | /** 最新版本 */ 19 | private String latestVersion; 20 | 21 | /** 是否有更新 */ 22 | private boolean hasUpdate; 23 | 24 | /** 发布URL */ 25 | private String releaseUrl; 26 | 27 | /** 发布说明 */ 28 | private String releaseNotes; 29 | 30 | /** 检查时间 */ 31 | private LocalDateTime checkTime; 32 | 33 | /** 是否为预发布版本 */ 34 | private boolean prerelease; 35 | 36 | /** 发布时间 */ 37 | private LocalDateTime publishedAt; 38 | 39 | /** 错误信息 */ 40 | private String error; 41 | } 42 | -------------------------------------------------------------------------------- /frontend/app.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_3__create_openlist_config_table.sql: -------------------------------------------------------------------------------- 1 | -- 创建openlist配置表 2 | CREATE TABLE openlist_config 3 | ( 4 | id INTEGER PRIMARY KEY AUTOINCREMENT, 5 | base_url VARCHAR(500) NOT NULL, 6 | token VARCHAR(1000) NOT NULL, 7 | base_path VARCHAR(500) DEFAULT '/', 8 | username VARCHAR(200) NOT NULL, 9 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 11 | is_active INTEGER DEFAULT 1 12 | ); 13 | 14 | -- 创建索引 15 | CREATE INDEX idx_openlist_config_username ON openlist_config(username); 16 | CREATE INDEX idx_openlist_config_active ON openlist_config(is_active); 17 | 18 | -- 创建触发器,自动更新updated_at字段 19 | CREATE TRIGGER update_openlist_config_updated_at 20 | AFTER UPDATE ON openlist_config 21 | FOR EACH ROW 22 | BEGIN 23 | UPDATE openlist_config SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 24 | END; -------------------------------------------------------------------------------- /backend/LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2024 OpenList STRM Project 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | For the full text of the GNU General Public License version 3, 20 | please visit: https://www.gnu.org/licenses/gpl-3.0.txt 21 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/version/GitHubRelease.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.version; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.time.LocalDateTime; 5 | import lombok.Data; 6 | 7 | /** 8 | * GitHub Release 信息DTO 9 | * 10 | * @author hienao 11 | * @since 2024-01-01 12 | */ 13 | @Data 14 | public class GitHubRelease { 15 | 16 | private String id; 17 | private String name; 18 | 19 | @JsonProperty("tag_name") 20 | private String tagName; 21 | 22 | private String body; 23 | private boolean draft; 24 | private boolean prerelease; 25 | 26 | @JsonProperty("created_at") 27 | private LocalDateTime createdAt; 28 | 29 | @JsonProperty("published_at") 30 | private LocalDateTime publishedAt; 31 | 32 | @JsonProperty("html_url") 33 | private String htmlUrl; 34 | 35 | @JsonProperty("assets") 36 | private GitHubAsset[] assets; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/middleware/docker-port.global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker端口映射全局中间件 3 | * 通用解决方案:确保在Docker环境下端口号不会在重定向时丢失 4 | */ 5 | 6 | export default defineNuxtRouteMiddleware((to, from) => { 7 | // 只在客户端执行 8 | if (import.meta.server) return 9 | 10 | console.log('Docker端口中间件执行:', { 11 | to: to.fullPath, 12 | from: from?.fullPath 13 | }) 14 | 15 | // 检查是否需要修复端口映射问题 16 | if (import.meta.client) { 17 | const currentOrigin = window.location.origin 18 | const currentHost = window.location.host 19 | 20 | // 如果当前访问使用了非标准端口,确保导航保持一致 21 | if (currentHost.includes(':') && !currentHost.endsWith(':80') && !currentHost.endsWith(':443')) { 22 | console.log('检测到非标准端口环境:', currentHost) 23 | 24 | // 检查目标路径是否可能导致端口丢失 25 | const targetUrl = to.fullPath 26 | if (targetUrl && !targetUrl.startsWith('http')) { 27 | // 相对路径导航,这是正常的,不需要特殊处理 28 | console.log('相对路径导航,无需修复') 29 | } 30 | } 31 | } 32 | 33 | console.log('Docker端口中间件检查完成') 34 | }) 35 | -------------------------------------------------------------------------------- /docs/log.md: -------------------------------------------------------------------------------- 1 | # 日志管理 2 | 3 | 本页面介绍应用的日志管理和查看方法。 4 | 5 | ## 日志文件位置 6 | 7 | ### Docker 环境 8 | - 容器内路径:`/maindata/log/` 9 | - 宿主机路径:`./logs/` 10 | 11 | ### 开发环境 12 | - 前端日志:`logs/frontend.log` 13 | - 后端日志:`logs/backend.log` 14 | - 错误日志:`logs/error.log` 15 | 16 | ## 日志查看方式 17 | 18 | ### Docker 容器查看 19 | ```bash 20 | # 查看容器日志 21 | docker logs -f ostrm 22 | 23 | # 进入容器查看日志文件 24 | docker exec -it ostrm tail -f /maindata/log/backend.log 25 | ``` 26 | 27 | ### 本地文件查看 28 | ```bash 29 | # 查看后端日志 30 | tail -f logs/backend.log 31 | 32 | # 查看前端日志 33 | tail -f logs/frontend.log 34 | 35 | # 查看错误日志 36 | tail -f logs/error.log 37 | ``` 38 | 39 | ## 日志级别配置 40 | 41 | 在系统设置中可以配置日志级别: 42 | - `DEBUG`: 详细调试信息 43 | - `INFO`: 一般信息(推荐) 44 | - `WARN`: 警告信息 45 | - `ERROR`: 错误信息 46 | 47 | ## 日志清理 48 | 49 | 系统会自动清理过期的日志文件,默认保留最近 7 天的日志。您也可以在系统设置中配置保留天数。 50 | 51 | ## 常见问题 52 | 53 | ### Q: 如何找到特定时间段的日志? 54 | A: 使用 `grep` 命令结合时间过滤: 55 | ```bash 56 | grep "2025-11-07" logs/backend.log 57 | ``` 58 | 59 | ### Q: 日志文件太大怎么办? 60 | A: 系统会自动轮转和清理日志,也可以手动删除旧日志文件。 61 | 62 | ### Q: 如何启用更详细的调试日志? 63 | A: 在系统设置中将日志级别改为 `DEBUG`。 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/entity/OpenlistConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * openlist配置信息实体类 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Data 15 | @EqualsAndHashCode(callSuper = false) 16 | @Accessors(chain = true) 17 | public class OpenlistConfig { 18 | 19 | /** 主键ID */ 20 | private Long id; 21 | 22 | /** openlist网址 */ 23 | private String baseUrl; 24 | 25 | /** 用户令牌 */ 26 | private String token; 27 | 28 | /** 初始路径 */ 29 | private String basePath; 30 | 31 | /** 用户名 */ 32 | private String username; 33 | 34 | /** 创建时间 */ 35 | private LocalDateTime createdAt; 36 | 37 | /** 更新时间 */ 38 | private LocalDateTime updatedAt; 39 | 40 | /** 是否启用:1-启用,0-禁用 */ 41 | private Boolean isActive; 42 | 43 | /** STRM文件生成时的baseUrl替换,可为空,为空时则不进行替换 */ 44 | private String strmBaseUrl; 45 | 46 | /** 是否启用URL编码:1-启用(默认),0-禁用 */ 47 | private Boolean enableUrlEncoding; 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/service/CacheService.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.service; 2 | 3 | import com.hienao.openlist2strm.config.cache.CacheConfig; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.cache.annotation.CacheEvict; 6 | import org.springframework.cache.annotation.CachePut; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | @Slf4j 12 | public class CacheService { 13 | @Cacheable(value = CacheConfig.VERIFY_CODE, key = "{#identify}", unless = "#result == null") 14 | public String getVerifyCodeBy(String identify) { 15 | return null; 16 | } 17 | 18 | @CachePut(value = CacheConfig.VERIFY_CODE, key = "{#identify}") 19 | public String upsertVerifyCodeBy(String identify, String value) { 20 | return value; 21 | } 22 | 23 | @CacheEvict(value = CacheConfig.VERIFY_CODE, key = "{#identify}") 24 | public void removeVerifyCodeBy(String identify) {} 25 | 26 | @CacheEvict(value = CacheConfig.VERIFY_CODE, allEntries = true) 27 | public void clearAllVerifyCode() {} 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_1__insert_urp_table.sql: -------------------------------------------------------------------------------- 1 | -- 插入用户权限相关的初始数据 2 | -- 注意:此应用使用基于文件的用户认证系统 (userInfo.json) 3 | -- 此迁移文件主要用于保持Flyway迁移序列的完整性 4 | 5 | -- 更新系统信息表,记录用户权限系统的配置 6 | INSERT INTO system_info (version, description) 7 | VALUES ('1.0.1', 'User authentication system configured (file-based)'); 8 | 9 | -- 创建一个配置表来存储系统级别的配置信息 10 | CREATE TABLE IF NOT EXISTS system_config ( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT, 12 | config_key VARCHAR(100) NOT NULL UNIQUE, 13 | config_value TEXT, 14 | description TEXT, 15 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 16 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 17 | ); 18 | 19 | -- 插入默认配置 20 | INSERT INTO system_config (config_key, config_value, description) 21 | VALUES 22 | ('auth_type', 'file_based', '认证类型:基于文件的用户认证'), 23 | ('user_info_path', './data/config/userInfo.json', '用户信息文件路径'), 24 | ('app_version', '1.0.0', '应用程序版本'); 25 | 26 | -- 创建触发器,自动更新updated_at字段 27 | CREATE TRIGGER update_system_config_updated_at 28 | AFTER UPDATE ON system_config 29 | FOR EACH ROW 30 | BEGIN 31 | UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 32 | END; -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/DataReportRequest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.time.Instant; 5 | import java.util.Map; 6 | import lombok.Data; 7 | 8 | /** 9 | * 数据上报请求DTO 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Data 15 | public class DataReportRequest { 16 | 17 | /** PostHog API Key */ 18 | @JsonProperty("api_key") 19 | private String apiKey = "phc_dT1G4XQQm5YJfodJbNhCavocArLqAIFI1m9H9IKxEUn"; 20 | 21 | /** 事件名称 */ 22 | private String event; 23 | 24 | /** 事件属性 */ 25 | private Map properties; 26 | 27 | /** 时间戳(ISO 8601格式) */ 28 | private String timestamp; 29 | 30 | /** 31 | * 构造函数 32 | * 33 | * @param event 事件名称 34 | * @param properties 事件属性 35 | */ 36 | public DataReportRequest(String event, Map properties) { 37 | this.event = event; 38 | this.properties = properties; 39 | this.timestamp = Instant.now().toString(); 40 | } 41 | 42 | /** 默认构造函数 */ 43 | public DataReportRequest() { 44 | this.timestamp = Instant.now().toString(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/ApplicationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * OpenList STRM - Stream Management System 3 | * Copyright (C) 2024 OpenList STRM Project 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package com.hienao.openlist2strm; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | 24 | @SpringBootApplication(scanBasePackages = {"com.hienao.openlist2strm"}) 25 | public class ApplicationService { 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(ApplicationService.class, args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/entity/TaskConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * 任务配置信息实体类 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Data 15 | @EqualsAndHashCode(callSuper = false) 16 | @Accessors(chain = true) 17 | public class TaskConfig { 18 | 19 | /** 主键ID */ 20 | private Long id; 21 | 22 | /** 任务名称 */ 23 | private String taskName; 24 | 25 | /** 任务路径 */ 26 | private String path; 27 | 28 | /** 关联的openlist_config表ID */ 29 | private Long openlistConfigId; 30 | 31 | /** 是否需要刮削:true-是,false-否 */ 32 | private Boolean needScrap; 33 | 34 | /** 重命名正则表达式,为空时表示不需要重命名 */ 35 | private String renameRegex; 36 | 37 | /** 定时任务表达式 */ 38 | private String cron; 39 | 40 | /** 是否是增量更新:true-是,false-否 */ 41 | private Boolean isIncrement; 42 | 43 | /** 生成strm的基础路径 */ 44 | private String strmPath; 45 | 46 | /** 上次执行的时间戳 */ 47 | private Long lastExecTime; 48 | 49 | /** 创建时间 */ 50 | private LocalDateTime createdAt; 51 | 52 | /** 更新时间 */ 53 | private LocalDateTime updatedAt; 54 | 55 | /** 是否启用:true-启用,false-禁用 */ 56 | private Boolean isActive; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./components/**/*.{js,vue,ts}", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.{js,ts}", 8 | "./app.vue", 9 | "./error.vue" 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Inter', 'system-ui', 'sans-serif'], 15 | }, 16 | colors: { 17 | primary: { 18 | 50: '#eff6ff', 19 | 100: '#dbeafe', 20 | 200: '#bfdbfe', 21 | 300: '#93c5fd', 22 | 400: '#60a5fa', 23 | 500: '#3b82f6', 24 | 600: '#2563eb', 25 | 700: '#1d4ed8', 26 | 800: '#1e40af', 27 | 900: '#1e3a8a', 28 | }, 29 | }, 30 | animation: { 31 | 'fade-in': 'fadeIn 0.5s ease-in-out', 32 | 'slide-up': 'slideUp 0.3s ease-out', 33 | }, 34 | keyframes: { 35 | fadeIn: { 36 | '0%': { opacity: '0' }, 37 | '100%': { opacity: '1' }, 38 | }, 39 | slideUp: { 40 | '0%': { transform: 'translateY(10px)', opacity: '0' }, 41 | '100%': { transform: 'translateY(0)', opacity: '1' }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | plugins: [ 47 | require('@tailwindcss/forms'), 48 | ], 49 | } -------------------------------------------------------------------------------- /backend/src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | # 统一路径配置 2 | app: 3 | paths: 4 | logs: ${APP_LOG_PATH:/maindata/log} 5 | data: ${APP_DATA_PATH:/maindata} 6 | database: ${APP_DATABASE_PATH:/maindata/db/openlist2strm.db} 7 | config: ${APP_CONFIG_PATH:/maindata/config} 8 | strm: ${APP_STRM_PATH:/app/backend/strm} 9 | userInfo: ${APP_USER_INFO_PATH:/maindata/config/userInfo.json} 10 | frontendLogs: ${APP_FRONTEND_LOGS_PATH:/maindata/log/frontend} 11 | 12 | server: 13 | port: 8080 14 | logging: 15 | file: 16 | path: ${app.paths.logs} 17 | spring: 18 | datasource: 19 | url: jdbc:sqlite:${app.paths.database} 20 | driver-class-name: org.sqlite.JDBC 21 | # MyBatis configuration 22 | flyway: 23 | enabled: true 24 | locations: classpath:db/migration 25 | default-schema: main 26 | mybatis: 27 | mapper-locations: classpath:mapper/*.xml 28 | type-aliases-package: com.hienao.openlist2strm.entity 29 | configuration: 30 | map-underscore-to-camel-case: true 31 | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl 32 | quartz: 33 | job-store-type: jdbc 34 | jdbc: 35 | initialize-schema: never 36 | auto-startup: true 37 | springdoc: 38 | swagger-ui: 39 | path: /swagger-ui.html 40 | jwt: 41 | secret: ${JWT_SECRET:secret} 42 | expiration-min: ${JWT_EXPIRATION_MIN:20160} # 14天 = 14 * 24 * 60 = 20160分钟 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/HttpFireWallConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hienao.openlist2strm.dto.ApiResponse; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.security.web.firewall.HttpFirewall; 10 | import org.springframework.security.web.firewall.RequestRejectedHandler; 11 | import org.springframework.security.web.firewall.StrictHttpFirewall; 12 | 13 | @Configuration 14 | public class HttpFireWallConfig { 15 | 16 | @Bean 17 | public HttpFirewall getHttpFirewall() { 18 | return new StrictHttpFirewall(); 19 | } 20 | 21 | @Bean 22 | public RequestRejectedHandler requestRejectedHandler() { 23 | return (request, response, requestRejectedException) -> { 24 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 25 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 26 | response.setCharacterEncoding("UTF-8"); 27 | 28 | ApiResponse result = 29 | ApiResponse.error(400, "请求被拒绝: " + requestRejectedException.getMessage()); 30 | 31 | ObjectMapper mapper = new ObjectMapper(); 32 | mapper.writeValue(response.getOutputStream(), result); 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | log { 3 | output file /var/log/caddy/access.log 4 | level INFO 5 | } 6 | admin off 7 | } 8 | 9 | :80 { 10 | root * /var/www/html 11 | file_server 12 | 13 | # API 代理配置 14 | reverse_proxy /api/* localhost:8080 { 15 | header_up Host {http.reverse_proxy.upstream.hostport} 16 | header_up X-Real-IP {http.request.remote_host} 17 | header_up X-Forwarded-For {http.request.remote_host} 18 | header_up X-Forwarded-Proto {scheme} 19 | header_up X-Forwarded-Host {host} 20 | header_up X-Forwarded-Port {port} 21 | 22 | # 超时配置 23 | transport http { 24 | dial_timeout 30s 25 | read_timeout 30s 26 | write_timeout 30s 27 | } 28 | } 29 | 30 | # WebSocket 代理配置 31 | reverse_proxy /ws/* localhost:8080 { 32 | header_up Host {http.reverse_proxy.upstream.hostport} 33 | header_up X-Real-IP {http.request.remote_host} 34 | header_up X-Forwarded-For {http.request.remote_host} 35 | header_up X-Forwarded-Proto {scheme} 36 | header_up X-Forwarded-Host {host} 37 | header_up X-Forwarded-Port {port} 38 | 39 | # WebSocket 长连接超时 40 | transport http { 41 | dial_timeout 30s 42 | read_timeout 86400s # 24小时 43 | write_timeout 86400s # 24小时 44 | } 45 | } 46 | 47 | # 健康检查端点 48 | handle /health { 49 | respond "healthy" 200 50 | } 51 | } -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_4__create_task_config_table.sql: -------------------------------------------------------------------------------- 1 | -- 创建任务配置表 2 | -- 字段说明: 3 | -- task_name: 任务名称 4 | -- path: 任务路径 5 | -- openlist_config_id: 关联的openlist_config表ID 6 | -- need_scrap: 是否需要刮削,0-否,1-是 7 | -- need_rename: 是否需要重命名,0-否,1-是 8 | -- cron: 定时任务表达式 9 | -- is_increment: 是否是增量更新,0-否,1-是 10 | -- strm_path: 生成strm的基础路径 11 | -- last_exec_time: 上次执行的时间戳 12 | -- is_active: 是否启用,0-禁用,1-启用 13 | CREATE TABLE task_config 14 | ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | task_name VARCHAR(200) NOT NULL, 17 | path VARCHAR(500) NOT NULL, 18 | openlist_config_id INTEGER NOT NULL, 19 | need_scrap INTEGER DEFAULT 0, 20 | need_rename INTEGER DEFAULT 0, 21 | cron VARCHAR(100) DEFAULT '', 22 | is_increment INTEGER DEFAULT 1, 23 | strm_path VARCHAR(500) DEFAULT '/strm/', 24 | last_exec_time BIGINT DEFAULT 0, 25 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 26 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 27 | is_active INTEGER DEFAULT 1 28 | ); 29 | 30 | -- 创建索引 31 | CREATE INDEX idx_task_config_task_name ON task_config(task_name); 32 | CREATE INDEX idx_task_config_path ON task_config(path); 33 | CREATE INDEX idx_task_config_openlist_config_id ON task_config(openlist_config_id); 34 | CREATE INDEX idx_task_config_active ON task_config(is_active); 35 | CREATE INDEX idx_task_config_cron ON task_config(cron); 36 | 37 | -- 创建触发器,自动更新updated_at字段 38 | CREATE TRIGGER update_task_config_updated_at 39 | AFTER UPDATE ON task_config 40 | FOR EACH ROW 41 | BEGIN 42 | UPDATE task_config SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 43 | END; -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/openlist/OpenlistConfigDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.openlist; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import jakarta.validation.constraints.Size; 6 | import java.time.LocalDateTime; 7 | import lombok.Data; 8 | import lombok.experimental.Accessors; 9 | 10 | /** 11 | * openlist配置DTO 12 | * 13 | * @author hienao 14 | * @since 2024-01-01 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | public class OpenlistConfigDto { 19 | 20 | /** 主键ID */ 21 | private Long id; 22 | 23 | /** openlist网址 */ 24 | @NotBlank(message = "openlist网址不能为空") @Pattern(regexp = "^https?://.*", message = "openlist网址格式不正确,必须以http://或https://开头") @Size(max = 500, message = "openlist网址长度不能超过500个字符") 25 | private String baseUrl; 26 | 27 | /** 用户令牌 */ 28 | @NotBlank(message = "用户令牌不能为空") @Size(max = 1000, message = "用户令牌长度不能超过1000个字符") private String token; 29 | 30 | /** 初始路径 */ 31 | @Size(max = 500, message = "初始路径长度不能超过500个字符") private String basePath; 32 | 33 | /** 用户名 */ 34 | @NotBlank(message = "用户名不能为空") @Size(max = 200, message = "用户名长度不能超过200个字符") private String username; 35 | 36 | /** 创建时间 */ 37 | private LocalDateTime createdAt; 38 | 39 | /** 更新时间 */ 40 | private LocalDateTime updatedAt; 41 | 42 | /** 是否启用:true-启用,false-禁用 */ 43 | private Boolean isActive; 44 | 45 | /** STRM文件生成时的baseUrl替换,可为空,为空时则不进行替换 */ 46 | @Size(max = 500, message = "STRM Base URL长度不能超过500个字符") private String strmBaseUrl; 47 | 48 | /** 是否启用URL编码:true-启用(默认),false-禁用 */ 49 | private Boolean enableUrlEncoding; 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/TaskExecutorConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import java.util.concurrent.Executor; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.scheduling.annotation.EnableAsync; 9 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 10 | 11 | /** 12 | * 任务执行线程池配置 13 | * 14 | * @author hienao 15 | * @since 2024-01-01 16 | */ 17 | @Slf4j 18 | @Configuration 19 | @EnableAsync 20 | public class TaskExecutorConfig { 21 | 22 | /** 任务提交线程池 线程数为1,容量为100000 */ 23 | @Bean("taskSubmitExecutor") 24 | public Executor taskSubmitExecutor() { 25 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 26 | 27 | // 核心线程数 28 | executor.setCorePoolSize(1); 29 | // 最大线程数 30 | executor.setMaxPoolSize(1); 31 | // 队列容量 32 | executor.setQueueCapacity(100000); 33 | // 线程名前缀 34 | executor.setThreadNamePrefix("task-submit-"); 35 | // 拒绝策略:调用者运行 36 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 37 | // 等待所有任务结束后再关闭线程池 38 | executor.setWaitForTasksToCompleteOnShutdown(true); 39 | // 等待时间 40 | executor.setAwaitTerminationSeconds(60); 41 | 42 | executor.initialize(); 43 | 44 | log.info( 45 | "任务提交线程池初始化完成 - 核心线程数: {}, 最大线程数: {}, 队列容量: {}", 46 | executor.getCorePoolSize(), 47 | executor.getMaxPoolSize(), 48 | executor.getQueueCapacity()); 49 | 50 | return executor; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 统一API响应格式 所有接口都应该返回这种格式的响应 9 | * 10 | * @param 数据类型,根据具体接口的返回数据定义 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class ApiResponse { 16 | 17 | /** 响应状态码 200: 成功 其他: 失败 */ 18 | private int code; 19 | 20 | /** 响应消息 */ 21 | private String message; 22 | 23 | /** 响应数据,类型根据具体接口定义 */ 24 | private T data; 25 | 26 | /** 27 | * 成功响应 28 | * 29 | * @param data 响应数据 30 | * @param 数据类型 31 | * @return ApiResponse 32 | */ 33 | public static ApiResponse success(T data) { 34 | return new ApiResponse<>(200, "success", data); 35 | } 36 | 37 | /** 38 | * 成功响应(带自定义消息) 39 | * 40 | * @param data 响应数据 41 | * @param message 自定义消息 42 | * @param 数据类型 43 | * @return ApiResponse 44 | */ 45 | public static ApiResponse success(T data, String message) { 46 | return new ApiResponse<>(200, message, data); 47 | } 48 | 49 | /** 50 | * 失败响应 51 | * 52 | * @param code 错误码 53 | * @param message 错误消息 54 | * @param 数据类型 55 | * @return ApiResponse 56 | */ 57 | public static ApiResponse error(int code, String message) { 58 | return new ApiResponse<>(code, message, null); 59 | } 60 | 61 | /** 62 | * 失败响应(默认500错误码) 63 | * 64 | * @param message 错误消息 65 | * @param 数据类型 66 | * @return ApiResponse 67 | */ 68 | public static ApiResponse error(String message) { 69 | return new ApiResponse<>(500, message, null); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/mapper/OpenlistConfigMapper.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.mapper; 2 | 3 | import com.hienao.openlist2strm.entity.OpenlistConfig; 4 | import java.util.List; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | /** 9 | * openlist配置信息Mapper接口 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Mapper 15 | public interface OpenlistConfigMapper { 16 | 17 | /** 18 | * 根据ID查询配置 19 | * 20 | * @param id 主键ID 21 | * @return 配置信息 22 | */ 23 | OpenlistConfig selectById(@Param("id") Long id); 24 | 25 | /** 26 | * 根据用户名查询配置 27 | * 28 | * @param username 用户名 29 | * @return 配置信息 30 | */ 31 | OpenlistConfig selectByUsername(@Param("username") String username); 32 | 33 | /** 34 | * 查询所有启用的配置 35 | * 36 | * @return 配置列表 37 | */ 38 | List selectActiveConfigs(); 39 | 40 | /** 41 | * 查询所有配置 42 | * 43 | * @return 配置列表 44 | */ 45 | List selectAll(); 46 | 47 | /** 48 | * 插入配置 49 | * 50 | * @param config 配置信息 51 | * @return 影响行数 52 | */ 53 | int insert(OpenlistConfig config); 54 | 55 | /** 56 | * 更新配置 57 | * 58 | * @param config 配置信息 59 | * @return 影响行数 60 | */ 61 | int updateById(OpenlistConfig config); 62 | 63 | /** 64 | * 根据ID删除配置 65 | * 66 | * @param id 主键ID 67 | * @return 影响行数 68 | */ 69 | int deleteById(@Param("id") Long id); 70 | 71 | /** 72 | * 启用/禁用配置 73 | * 74 | * @param id 主键ID 75 | * @param isActive 是否启用 76 | * @return 影响行数 77 | */ 78 | int updateActiveStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/task/TaskConfigDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.task; 2 | 3 | import com.hienao.openlist2strm.validation.ValidCronExpression; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Size; 6 | import java.time.LocalDateTime; 7 | import lombok.Data; 8 | import lombok.experimental.Accessors; 9 | 10 | /** 11 | * 任务配置DTO 12 | * 13 | * @author hienao 14 | * @since 2024-01-01 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | public class TaskConfigDto { 19 | 20 | /** 主键ID */ 21 | private Long id; 22 | 23 | /** 任务名称 */ 24 | @NotBlank(message = "任务名称不能为空") @Size(max = 200, message = "任务名称长度不能超过200个字符") private String taskName; 25 | 26 | /** 任务路径 */ 27 | @NotBlank(message = "任务路径不能为空") @Size(max = 500, message = "任务路径长度不能超过500个字符") private String path; 28 | 29 | /** 关联的openlist_config表ID */ 30 | private Long openlistConfigId; 31 | 32 | /** 是否需要刮削:true-需要,false-不需要 */ 33 | private Boolean needScrap; 34 | 35 | /** 重命名正则表达式,为空时表示不需要重命名 */ 36 | @Size(max = 500, message = "重命名正则表达式长度不能超过500个字符") private String renameRegex; 37 | 38 | /** 定时任务表达式 */ 39 | @Size(max = 100, message = "定时任务表达式长度不能超过100个字符") @ValidCronExpression(message = "定时任务表达式格式不正确") 40 | private String cron; 41 | 42 | /** 是否是增量更新:true-是,false-否 */ 43 | private Boolean isIncrement; 44 | 45 | /** 生成strm的基础路径 */ 46 | @Size(max = 500, message = "strm路径长度不能超过500个字符") private String strmPath; 47 | 48 | /** 上次执行的时间戳 */ 49 | private Long lastExecTime; 50 | 51 | /** 创建时间 */ 52 | private LocalDateTime createdAt; 53 | 54 | /** 更新时间 */ 55 | private LocalDateTime updatedAt; 56 | 57 | /** 是否启用:true-启用,false-禁用 */ 58 | private Boolean isActive; 59 | } 60 | -------------------------------------------------------------------------------- /.kilocode/rules/memory-bank/product.md: -------------------------------------------------------------------------------- 1 | # 产品描述 - OpenList to Stream 2 | 3 | ## 产品愿景 4 | 5 | OpenList to Stream 是一个现代化的全栈Web应用程序,旨在将OpenList文件列表自动转换为STRM流媒体文件。该系统通过智能化的任务调度和媒体刮削技术,为用户提供一站式的媒体文件管理解决方案。 6 | 7 | ## 核心价值主张 8 | 9 | ### 解决的问题 10 | 1. **手动转换效率低下**:传统手动将OpenList文件列表转换为STRM文件的过程繁琐且容易出错 11 | 2. **媒体信息缺失**:缺乏自动化的媒体元数据刮削功能,影响媒体库的完整性和美观度 12 | 3. **任务管理困难**:缺乏统一的任务管理和调度平台,难以批量处理大量文件 13 | 4. **增量更新复杂**:手动处理增量更新和清理孤立文件的工作量巨大 14 | 15 | ### 提供的解决方案 16 | 1. **自动化转换流程**:一键将OpenList文件列表转换为STRM流媒体文件 17 | 2. **智能媒体刮削**:集成AI技术和TMDB API,自动获取电影、电视剧的详细信息 18 | 3. **灵活的任务调度**:支持Cron表达式的定时任务,满足不同使用场景 19 | 4. **增量更新支持**:智能识别和处理增量更新,提高处理效率 20 | 5. **用户友好界面**:直观的Web管理界面,支持多设备访问 21 | 22 | ## 目标用户群体 23 | 24 | ### 主要用户 25 | 1. **个人媒体库管理员**:维护个人媒体库的用户,需要定期更新媒体文件 26 | 2. **小型媒体服务提供商**:为用户提供媒体服务的中小型提供商 27 | 3. **技术爱好者**:喜欢自动化和智能化解决方案的技术用户 28 | 29 | ### 用户场景 30 | 1. **日常维护**:定期从OpenList服务器同步最新的媒体文件 31 | 2. **批量迁移**:将大量媒体文件从OpenList迁移到支持STRM的媒体服务器 32 | 3. **元数据完善**:为现有媒体文件添加缺失的封面、简介等信息 33 | 4. **自动化备份**:定时备份和同步媒体文件,确保数据完整性 34 | 35 | ## 产品特色 36 | 37 | ### 技术特色 38 | 1. **全栈现代化架构**:前端使用Nuxt.js 3,后端使用Spring Boot,确保高性能和可维护性 39 | 2. **容器化部署**:完整的Docker支持,简化部署和运维流程 40 | 3. **轻量级数据存储**:使用SQLite数据库,降低资源消耗 41 | 4. **异步任务处理**:基于Quartz的任务调度,支持高并发处理 42 | 43 | ### 功能特色 44 | 1. **智能文件解析**:结合正则表达式和AI技术,准确识别媒体文件信息 45 | 2. **灵活的配置管理**:支持多个OpenList服务器配置,满足不同需求 46 | 3. **实时监控**:提供任务执行状态实时监控和日志查看 47 | 4. **安全认证**:基于JWT的安全认证系统,保护用户数据安全 48 | 49 | ### 用户体验特色 50 | 1. **直观的操作界面**:现代化的UI设计,操作简单直观 51 | 2. **响应式设计**:完美适配桌面端和移动端设备 52 | 3. **实时反馈**:提供操作进度和结果反馈,增强用户体验 53 | 4. **错误处理**:完善的错误提示和异常处理机制 54 | 55 | ## 产品定位 56 | 57 | OpenList to Stream 定位为个人和小型团队使用的媒体文件管理工具,专注于提供简单易用、功能完善的STRM文件生成解决方案。与市场上复杂的媒体服务器解决方案不同,我们专注于单一核心功能,确保产品的稳定性和易用性。 58 | 59 | ## 发展愿景 60 | 61 | 未来计划扩展支持更多文件列表格式(如Alist、PikPak等),增强AI识别能力,提供更丰富的媒体刮削选项,并可能增加插件化架构支持第三方扩展。 -------------------------------------------------------------------------------- /backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 统一路径配置 2 | app: 3 | paths: 4 | logs: ${APP_LOG_PATH:/maindata/log} 5 | data: ${APP_DATA_PATH:/maindata} 6 | database: ${APP_DATABASE_PATH:/maindata/db/openlist2strm.db} 7 | config: ${APP_CONFIG_PATH:/maindata/config} 8 | strm: ${APP_STRM_PATH:/app/backend/strm} 9 | userInfo: ${APP_USER_INFO_PATH:/maindata/config/userInfo.json} 10 | frontendLogs: ${APP_FRONTEND_LOGS_PATH:/maindata/log/frontend} 11 | 12 | server: 13 | port: 8080 14 | logging: 15 | file: 16 | path: ${app.paths.logs} 17 | spring: 18 | datasource: 19 | url: jdbc:sqlite:${app.paths.database} 20 | driver-class-name: org.sqlite.JDBC 21 | # SQLite JDBC driver compatible with Alpine Linux 22 | hikari: 23 | connection-timeout: 30000 24 | maximum-pool-size: 10 25 | minimum-idle: 5 26 | # MyBatis configuration 27 | flyway: 28 | enabled: true 29 | locations: classpath:db/migration 30 | default-schema: main 31 | out-of-order: true 32 | mybatis: 33 | mapper-locations: classpath:mapper/*.xml 34 | type-aliases-package: com.hienao.openlist2strm.entity 35 | configuration: 36 | map-underscore-to-camel-case: true 37 | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl 38 | quartz: 39 | job-store-type: jdbc 40 | jdbc: 41 | initialize-schema: never 42 | auto-startup: true 43 | springdoc: 44 | swagger-ui: 45 | path: /swagger-ui.html 46 | jwt: 47 | secret: ${JWT_SECRET:secret} 48 | expiration-min: ${JWT_EXPIRATION_MIN:20160} # 14天 = 14 * 24 * 60 = 20160分钟 49 | 50 | # GitHub配置 51 | github: 52 | repo: 53 | owner: ${GITHUB_REPO_OWNER:hienao} 54 | name: ${GITHUB_REPO_NAME:ostrm} 55 | api: 56 | timeout: ${GITHUB_API_TIMEOUT:30} 57 | retry-count: ${GITHUB_API_RETRY_COUNT:3} 58 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * OpenList STRM - Stream Management System 3 | * Copyright (C) 2024 OpenList STRM Project 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package com.hienao.openlist2strm.config; 20 | 21 | import com.hienao.openlist2strm.component.LogWebSocketHandler; 22 | import lombok.RequiredArgsConstructor; 23 | import org.springframework.context.annotation.Configuration; 24 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 25 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 26 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 27 | 28 | @Configuration 29 | @EnableWebSocket 30 | @RequiredArgsConstructor 31 | public class WebSocketConfig implements WebSocketConfigurer { 32 | 33 | private final LogWebSocketHandler logWebSocketHandler; 34 | 35 | @Override 36 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 37 | registry 38 | .addHandler(logWebSocketHandler, "/ws/logs/{logType}") 39 | .setAllowedOrigins("*"); // 在生产环境中应该限制允许的源 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/plugins/docker-port-fix.client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker端口映射修复插件 3 | * 通用解决方案:解决容器内80端口映射到外部端口时的重定向问题 4 | */ 5 | 6 | export default defineNuxtPlugin(async () => { 7 | // 只在客户端运行 8 | if (import.meta.server) return 9 | 10 | const logger = await import('~/utils/logger.js').then(m => m.default) 11 | 12 | logger.info('Docker端口修复插件启动') 13 | 14 | // 监听页面导航事件,确保URL保持一致性 15 | const handleNavigation = () => { 16 | const currentOrigin = window.location.origin 17 | 18 | // 检查是否存在端口映射但URL不一致的情况 19 | // 通过比较当前访问的origin和页面中可能出现的重定向URL 20 | const links = document.querySelectorAll('a[href]') 21 | links.forEach(link => { 22 | const href = link.getAttribute('href') 23 | if (href && href.startsWith('http') && !href.startsWith(currentOrigin)) { 24 | // 检查是否是同一个域名但端口不同的情况 25 | try { 26 | const linkUrl = new URL(href) 27 | const currentUrl = new URL(currentOrigin) 28 | 29 | if (linkUrl.hostname === currentUrl.hostname && linkUrl.port !== currentUrl.port) { 30 | // 修复链接,使用当前的origin 31 | const correctedHref = href.replace(linkUrl.origin, currentOrigin) 32 | link.setAttribute('href', correctedHref) 33 | logger.info('修复链接:', href, '->', correctedHref) 34 | } 35 | } catch (e) { 36 | // 忽略无效URL 37 | } 38 | } 39 | }) 40 | } 41 | 42 | // 页面加载完成后执行检查 43 | if (document.readyState === 'loading') { 44 | document.addEventListener('DOMContentLoaded', handleNavigation) 45 | } else { 46 | handleNavigation() 47 | } 48 | 49 | // 监听DOM变化,处理动态生成的链接 50 | const observer = new MutationObserver(handleNavigation) 51 | observer.observe(document.body, { 52 | childList: true, 53 | subtree: true 54 | }) 55 | 56 | logger.info('Docker端口修复插件已激活') 57 | }) 58 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/cache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.cache; 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | import org.springframework.cache.CacheManager; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.cache.caffeine.CaffeineCache; 9 | import org.springframework.cache.support.SimpleCacheManager; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @EnableCaching 14 | @Configuration 15 | public class CacheConfig { 16 | 17 | public static final String VERIFY_CODE = "verifyCode"; 18 | public static final String VERSION_CHECK = "versionCheck"; 19 | public static final String GITHUB_RELEASES = "githubReleases"; 20 | 21 | @Bean 22 | public CacheManager cacheManager() { 23 | SimpleCacheManager cacheManager = new SimpleCacheManager(); 24 | cacheManager.setCaches(List.of(verifyCodeCache(), versionCheckCache(), githubReleasesCache())); 25 | return cacheManager; 26 | } 27 | 28 | private CaffeineCache verifyCodeCache() { 29 | return new CaffeineCache( 30 | VERIFY_CODE, 31 | Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(60, TimeUnit.SECONDS).build()); 32 | } 33 | 34 | private CaffeineCache versionCheckCache() { 35 | return new CaffeineCache( 36 | VERSION_CHECK, 37 | Caffeine.newBuilder().maximumSize(100).expireAfterWrite(1, TimeUnit.HOURS).build()); 38 | } 39 | 40 | private CaffeineCache githubReleasesCache() { 41 | return new CaffeineCache( 42 | GITHUB_RELEASES, 43 | Caffeine.newBuilder().maximumSize(10).expireAfterWrite(6, TimeUnit.HOURS).build()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/DOCKER_SETUP.md: -------------------------------------------------------------------------------- 1 | # Docker Hub 自动化部署配置说明 2 | 3 | ## 概述 4 | 5 | 本项目已配置 GitHub Actions 工作流,当代码合并到 `main` 分支时,会自动构建 Docker 镜像并推送到 Docker Hub。 6 | 7 | ## 配置步骤 8 | 9 | ### 1. 创建 Docker Hub 访问令牌 10 | 11 | 1. 登录 [Docker Hub](https://hub.docker.com/) 12 | 2. 点击右上角头像 → Account Settings 13 | 3. 选择 Security 标签页 14 | 4. 点击 "New Access Token" 15 | 5. 输入令牌名称(如:`github-actions`) 16 | 6. 选择权限:`Read, Write, Delete` 17 | 7. 点击 "Generate" 并**立即复制令牌**(只显示一次) 18 | 19 | ### 2. 在 GitHub 仓库中配置 Variables 和 Secrets 20 | 21 | 1. 进入 GitHub 仓库页面 22 | 2. 点击 Settings 标签页 23 | 3. 在左侧菜单中选择 "Secrets and variables" → "Actions" 24 | 25 | #### 配置 Variables: 26 | 4. 点击 "Variables" 标签 27 | 5. 点击 "New repository variable" 添加以下变量: 28 | 29 | - **Name**: `DOCKERHUB_USERNAME` 30 | **Value**: 你的 Docker Hub 用户名 31 | 32 | #### 配置 Secrets: 33 | 6. 点击 "Secrets" 标签 34 | 7. 点击 "New repository secret" 添加以下密钥: 35 | 36 | - **Name**: `DOCKERHUB_TOKEN` 37 | **Value**: 刚才创建的访问令牌 38 | 39 | ### 3. 工作流触发条件 40 | 41 | - **自动触发**:当代码推送到 `main` 分支时 42 | - **手动触发**:在 Actions 页面可以手动运行工作流 43 | - **PR 检查**:Pull Request 时会构建镜像但不推送 44 | 45 | ### 4. 镜像标签策略 46 | 47 | - `latest`:main 分支的最新版本 48 | - `main-{commit-sha}`:基于提交 SHA 的标签 49 | - `{branch-name}`:分支名称标签 50 | 51 | ### 5. 支持的架构 52 | 53 | - `linux/amd64`:x86_64 架构 54 | - `linux/arm64`:ARM64 架构(如 Apple Silicon) 55 | 56 | ## 使用镜像 57 | 58 | 配置完成后,可以通过以下命令拉取和运行镜像: 59 | 60 | ```bash 61 | # 拉取最新镜像 62 | docker pull hienao6/ostrm:latest 63 | 64 | # 运行容器 65 | docker run -d -p 80:80 -p 8080:8080 hienao6/ostrm:latest 66 | ``` 67 | 68 | ## 故障排除 69 | 70 | 1. **构建失败**:检查 Dockerfile 语法和依赖项 71 | 2. **推送失败**:验证 Docker Hub 凭据是否正确配置 72 | 3. **权限错误**:确保访问令牌具有足够的权限 73 | 4. **标签格式错误** (`invalid tag "docker.io/***/ostrm:main"`): 74 | - 原因:`DOCKERHUB_USERNAME` variable 未配置 75 | - 解决:按照上述步骤 2 配置 GitHub Variables 和 Secrets 76 | 77 | ## 工作流文件位置 78 | 79 | `.github/workflows/docker-build-push.yml` -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import java.time.Duration; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | /** 13 | * RestTemplate配置类 14 | * 15 | * @author hienao 16 | * @since 2024-01-01 17 | */ 18 | @Configuration 19 | public class RestTemplateConfig { 20 | 21 | /** 22 | * 配置RestTemplate Bean 23 | * 24 | * @return RestTemplate实例 25 | */ 26 | @Bean 27 | public RestTemplate restTemplate() { 28 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 29 | 30 | // 设置连接超时时间(毫秒) 31 | factory.setConnectTimeout((int) Duration.ofSeconds(30).toMillis()); 32 | 33 | // 设置读取超时时间(毫秒) 34 | factory.setReadTimeout((int) Duration.ofSeconds(60).toMillis()); 35 | 36 | // 启用重定向跟随 37 | factory.setOutputStreaming(false); 38 | 39 | RestTemplate restTemplate = new RestTemplate(factory); 40 | 41 | // 添加拦截器 42 | restTemplate.setInterceptors(getInterceptors()); 43 | 44 | return restTemplate; 45 | } 46 | 47 | /** 48 | * 获取拦截器列表 49 | * 50 | * @return 拦截器列表 51 | */ 52 | private List getInterceptors() { 53 | List interceptors = new ArrayList<>(); 54 | 55 | // 添加用户代理拦截器 56 | interceptors.add( 57 | (request, body, execution) -> { 58 | request.getHeaders().set("User-Agent", "OpenList-STRM/1.0"); 59 | return execution.execute(request, body); 60 | }); 61 | 62 | return interceptors; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import java.util.Arrays; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.cors.CorsConfiguration; 7 | import org.springframework.web.cors.CorsConfigurationSource; 8 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 9 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | 12 | @Configuration 13 | public class CorsConfig implements WebMvcConfigurer { 14 | 15 | // CORS配置 - 开发环境和生产环境都使用通配符 16 | private static final String[] ALLOWED_ORIGINS = {"*"}; 17 | 18 | private static final String[] ALLOWED_METHODS = {"*"}; 19 | private static final String[] ALLOWED_HEADERS = {"*"}; 20 | private static final String[] ALLOWED_EXPOSE_HEADERS = {"*"}; 21 | 22 | @Override 23 | public void addCorsMappings(CorsRegistry registry) { 24 | registry.addMapping("/**"); 25 | } 26 | 27 | @Bean 28 | public CorsConfigurationSource corsConfigurationSource() { 29 | CorsConfiguration configuration = new CorsConfiguration(); 30 | 31 | // 设置允许的源(包含通配符) 32 | configuration.setAllowedOriginPatterns(Arrays.asList(ALLOWED_ORIGINS)); 33 | 34 | // 设置允许的方法 35 | configuration.setAllowedMethods(Arrays.asList(ALLOWED_METHODS)); 36 | 37 | // 设置允许的头部 38 | configuration.setAllowedHeaders(Arrays.asList(ALLOWED_HEADERS)); 39 | 40 | // 设置暴露的头部 41 | configuration.setExposedHeaders(Arrays.asList(ALLOWED_EXPOSE_HEADERS)); 42 | 43 | // 允许携带认证信息 44 | configuration.setAllowCredentials(true); 45 | 46 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 47 | source.registerCorsConfiguration("/**", configuration); 48 | return source; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.env.docker.example: -------------------------------------------------------------------------------- 1 | # Docker部署环境变量配置示例 2 | # 复制此文件为 .env 3 | 4 | # 宿主机路径配置(将映射到容器内的标准化路径) 5 | # 这些路径将自动映射到容器内的统一目录结构 6 | 7 | # 日志文件存储路径(宿主机路径) 8 | LOG_PATH_HOST=./logs 9 | 10 | # 配置文件存储路径(宿主机路径) 11 | CONFIG_PATH_HOST=./data/config 12 | 13 | # 数据库文件存储路径(宿主机路径) 14 | DB_PATH_HOST=./data/db 15 | 16 | # STRM文件输出路径(宿主机路径) 17 | STRM_PATH_HOST=./strm 18 | 19 | # 容器内部标准化路径(不建议直接修改) 20 | # - /maindata/log/ - 统一日志目录 21 | # - /maindata/config/ - 统一配置文件目录 22 | # - /maindata/db/ - 统一数据库目录 23 | # - /app/backend/strm/ - 统一STRM文件目录(不变) 24 | 25 | # Docker部署说明: 26 | # 1. 启动服务: 27 | # docker-compose up -d 28 | # 29 | # 2. 端口配置: 30 | # - 容器内部固定使用80端口 31 | # - 外部端口映射在docker-compose.yml中配置(默认3111) 32 | # - 用户可根据需要修改docker-compose.yml中的端口映射 33 | # 34 | # 3. 目录挂载说明(已标准化): 35 | # - ./logs → /maindata/log (日志文件存储) 36 | # - ./data/config → /maindata/config (配置文件存储) 37 | # - ./data/db → /maindata/db (数据库文件存储) 38 | # - ./strm → /app/backend/strm (STRM文件输出目录) 39 | # 40 | # 4. 访问地址示例: 41 | # http://localhost:3111 (如果外部端口映射为3111) 42 | # http://your-domain.com:3111 43 | # 44 | # 5. 路径标准化说明: 45 | # - 所有路径现已统一为标准化格式 46 | # - 容器内使用 /maindata/ 作为统一数据存储根目录 47 | # - 日志文件统一存储在 /maindata/log/ 目录下 48 | # - 配置文件统一存储在 /maindata/config/ 目录下 49 | # - 数据库文件统一存储在 /maindata/db/ 目录下 50 | # - STRM文件仍保持在 /app/backend/strm/ 目录下 51 | # - 支持通过环境变量灵活配置宿主机路径 52 | # - 向后兼容:现有部署无需修改 53 | # 54 | # 6. 重要提醒: 55 | # - CORS配置已写死在应用中,支持所有域名和端口 56 | # - 无需手动配置CORS相关环境变量 57 | # - 刷新页面时会保持正确的端口号 58 | # 59 | # 7. GitHub版本检查配置: 60 | # - GITHUB_REPO_OWNER: GitHub仓库所有者(默认:hienao) 61 | # - GITHUB_REPO_NAME: GitHub仓库名称(默认:ostrm) 62 | # - GITHUB_API_TIMEOUT: GitHub API超时时间(默认:30秒) 63 | # - APP_VERSION: 应用版本号(通过GitHub Actions自动设置) 64 | # 65 | # 8. 路径标准化的好处: 66 | # - 配置一致性:所有组件使用统一的路径标准 67 | # - 清晰分离:日志、配置、数据库文件分别存储 68 | # - 向后兼容:现有用户无需修改现有部署 69 | # - 灵活部署:支持多种部署环境和自定义路径 70 | # - 便于维护:集中化的路径管理机制 71 | # - 跨平台兼容:Works consistently across different host operating systems 72 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | lang: 'zh_CN', 6 | title: "OpenList-Strm", 7 | description: "便捷的为你的OpenList影音文件生成Strm文件", 8 | // 使用 alpha 版本的配置 9 | // 注意:alpha 版本可能不支持 ignoreDeadLinks 10 | themeConfig: { 11 | // https://vitepress.dev/reference/default-theme-config 12 | nav: [ 13 | { text: '首页', link: '/' }, 14 | { text: '功能介绍',items: [ 15 | { text: '快速开始', link: '/quick-start' }, 16 | { text: '添加OpenList', link: '/add-openlist' }, 17 | { text: '添加任务', link: '/add-task' }, 18 | { text: '系统设置', link: '/system-config' } 19 | ] }, 20 | { text: '特殊配置项说明', items: [ 21 | { text: 'STRM Base URL 配置', link: '/strm-base-url-config' }, 22 | { text: 'AI 识别配置', link: '/ai-recognition-config' }, 23 | { text: 'URL编码配置', link: '/url-encoding-config' } 24 | ] }, 25 | { text: '更新历史', link: '/update-log' }, 26 | { text: '参与开发', link: '/dev' }, 27 | { text: '常见问题', link: '/faq' } 28 | ], 29 | 30 | sidebar: [ 31 | { text: '首页', link: '/' }, 32 | { text: '功能介绍',items: [ 33 | { text: '快速开始', link: '/quick-start' }, 34 | { text: '添加OpenList', link: '/add-openlist' }, 35 | { text: '添加任务', link: '/add-task' }, 36 | { text: '系统设置', link: '/system-config' } 37 | ] }, 38 | { text: '特殊配置项说明', items: [ 39 | { text: 'STRM Base URL 配置', link: '/strm-base-url-config' }, 40 | { text: 'AI 识别配置', link: '/ai-recognition-config' }, 41 | { text: 'URL编码配置', link: '/url-encoding-config' } 42 | ] }, 43 | { text: '更新历史', link: '/update-log' }, 44 | { text: '参与开发', link: '/dev' }, 45 | { text: '常见问题', link: '/faq' } 46 | ], 47 | 48 | socialLinks: [ 49 | { icon: 'github', link: 'https://github.com/hienao/ostrm' } 50 | ] 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/JwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import java.io.Serial; 4 | import java.util.Collection; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.ToString; 8 | import org.springframework.security.authentication.AbstractAuthenticationToken; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.SpringSecurityCoreVersion; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | 13 | @Setter 14 | @Getter 15 | @ToString 16 | public class JwtAuthenticationToken extends AbstractAuthenticationToken { 17 | 18 | @Serial private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; 19 | 20 | private final Object principal; 21 | 22 | private String credentials; 23 | 24 | public JwtAuthenticationToken(Object principal, String credentials) { 25 | super(null); 26 | this.principal = principal; 27 | this.credentials = credentials; 28 | super.setAuthenticated(false); 29 | } 30 | 31 | public JwtAuthenticationToken( 32 | Object principal, String credentials, Collection authorities) { 33 | super(authorities); 34 | this.principal = principal; 35 | this.credentials = credentials; 36 | super.setAuthenticated(true); 37 | } 38 | 39 | public static JwtAuthenticationToken unauthenticated(String userIdentify, String token) { 40 | return new JwtAuthenticationToken(userIdentify, token); 41 | } 42 | 43 | public static JwtAuthenticationToken authenticated( 44 | UserDetails principal, String token, Collection authorities) { 45 | return new JwtAuthenticationToken(principal, token, authorities); 46 | } 47 | 48 | @Override 49 | public String getCredentials() { 50 | return this.credentials; 51 | } 52 | 53 | @Override 54 | public Object getPrincipal() { 55 | return this.principal; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.bmad-core/utils/workflow-management.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Workflow Management 4 | 5 | Enables BMad orchestrator to manage and execute team workflows. 6 | 7 | ## Dynamic Workflow Loading 8 | 9 | Read available workflows from current team configuration's `workflows` field. Each team bundle defines its own supported workflows. 10 | 11 | **Key Commands**: 12 | 13 | - `/workflows` - List workflows in current bundle or workflows folder 14 | - `/agent-list` - Show agents in current bundle 15 | 16 | ## Workflow Commands 17 | 18 | ### /workflows 19 | 20 | Lists available workflows with titles and descriptions. 21 | 22 | ### /workflow-start {workflow-id} 23 | 24 | Starts workflow and transitions to first agent. 25 | 26 | ### /workflow-status 27 | 28 | Shows current progress, completed artifacts, and next steps. 29 | 30 | ### /workflow-resume 31 | 32 | Resumes workflow from last position. User can provide completed artifacts. 33 | 34 | ### /workflow-next 35 | 36 | Shows next recommended agent and action. 37 | 38 | ## Execution Flow 39 | 40 | 1. **Starting**: Load definition → Identify first stage → Transition to agent → Guide artifact creation 41 | 42 | 2. **Stage Transitions**: Mark complete → Check conditions → Load next agent → Pass artifacts 43 | 44 | 3. **Artifact Tracking**: Track status, creator, timestamps in workflow_state 45 | 46 | 4. **Interruption Handling**: Analyze provided artifacts → Determine position → Suggest next step 47 | 48 | ## Context Passing 49 | 50 | When transitioning, pass: 51 | 52 | - Previous artifacts 53 | - Current workflow stage 54 | - Expected outputs 55 | - Decisions/constraints 56 | 57 | ## Multi-Path Workflows 58 | 59 | Handle conditional paths by asking clarifying questions when needed. 60 | 61 | ## Best Practices 62 | 63 | 1. Show progress 64 | 2. Explain transitions 65 | 3. Preserve context 66 | 4. Allow flexibility 67 | 5. Track state 68 | 69 | ## Agent Integration 70 | 71 | Agents should be workflow-aware: know active workflow, their role, access artifacts, understand expected outputs. 72 | -------------------------------------------------------------------------------- /.bmad-core/data/brainstorming-techniques.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Brainstorming Techniques Data 4 | 5 | ## Creative Expansion 6 | 7 | 1. **What If Scenarios**: Ask one provocative question, get their response, then ask another 8 | 2. **Analogical Thinking**: Give one example analogy, ask them to find 2-3 more 9 | 3. **Reversal/Inversion**: Pose the reverse question, let them work through it 10 | 4. **First Principles Thinking**: Ask "What are the fundamentals?" and guide them to break it down 11 | 12 | ## Structured Frameworks 13 | 14 | 5. **SCAMPER Method**: Go through one letter at a time, wait for their ideas before moving to next 15 | 6. **Six Thinking Hats**: Present one hat, ask for their thoughts, then move to next hat 16 | 7. **Mind Mapping**: Start with central concept, ask them to suggest branches 17 | 18 | ## Collaborative Techniques 19 | 20 | 8. **"Yes, And..." Building**: They give idea, you "yes and" it, they "yes and" back - alternate 21 | 9. **Brainwriting/Round Robin**: They suggest idea, you build on it, ask them to build on yours 22 | 10. **Random Stimulation**: Give one random prompt/word, ask them to make connections 23 | 24 | ## Deep Exploration 25 | 26 | 11. **Five Whys**: Ask "why" and wait for their answer before asking next "why" 27 | 12. **Morphological Analysis**: Ask them to list parameters first, then explore combinations together 28 | 13. **Provocation Technique (PO)**: Give one provocative statement, ask them to extract useful ideas 29 | 30 | ## Advanced Techniques 31 | 32 | 14. **Forced Relationships**: Connect two unrelated concepts and ask them to find the bridge 33 | 15. **Assumption Reversal**: Challenge their core assumptions and ask them to build from there 34 | 16. **Role Playing**: Ask them to brainstorm from different stakeholder perspectives 35 | 17. **Time Shifting**: "How would you solve this in 1995? 2030?" 36 | 18. **Resource Constraints**: "What if you had only $10 and 1 hour?" 37 | 19. **Metaphor Mapping**: Use extended metaphors to explore solutions 38 | 20. **Question Storming**: Generate questions instead of answers first 39 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/job/VersionCheckJob.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.job; 2 | 3 | import com.hienao.openlist2strm.dto.version.VersionCheckResponse; 4 | import com.hienao.openlist2strm.service.GitHubVersionService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.quartz.Job; 8 | import org.quartz.JobExecutionContext; 9 | import org.quartz.JobExecutionException; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * 版本检查定时任务 14 | * 15 | * @author hienao 16 | * @since 2024-01-01 17 | */ 18 | @Slf4j 19 | @Component 20 | @RequiredArgsConstructor 21 | public class VersionCheckJob implements Job { 22 | 23 | private final GitHubVersionService gitHubVersionService; 24 | 25 | @Override 26 | public void execute(JobExecutionContext context) throws JobExecutionException { 27 | try { 28 | log.info("开始执行版本检查定时任务"); 29 | 30 | // 获取当前版本 31 | String currentVersion = getCurrentVersion(); 32 | 33 | // 检查版本更新 34 | VersionCheckResponse response = gitHubVersionService.checkVersionUpdate(currentVersion); 35 | 36 | if (response.getError() != null) { 37 | log.warn("版本检查失败: {}", response.getError()); 38 | return; 39 | } 40 | 41 | if (response.isHasUpdate()) { 42 | log.info("发现新版本: 当前版本 {}, 最新版本 {}", currentVersion, response.getLatestVersion()); 43 | 44 | // 这里可以添加通知逻辑,比如发送邮件、WebSocket推送等 45 | // notifyNewVersion(response); 46 | } else { 47 | log.debug("当前版本已是最新: {}", currentVersion); 48 | } 49 | 50 | log.info("版本检查定时任务执行完成"); 51 | } catch (Exception e) { 52 | log.error("版本检查定时任务执行失败", e); 53 | throw new JobExecutionException("版本检查定时任务执行失败", e); 54 | } 55 | } 56 | 57 | /** 获取当前版本 */ 58 | private String getCurrentVersion() { 59 | // 从环境变量获取版本号(通过GitHub Actions自动设置) 60 | String version = System.getenv("APP_VERSION"); 61 | if (version == null || version.trim().isEmpty()) { 62 | version = "dev"; // 默认版本号 63 | } 64 | return version; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/RestfulAuthenticationEntryPointHandler.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hienao.openlist2strm.dto.ApiResponse; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.security.access.AccessDeniedException; 11 | import org.springframework.security.core.AuthenticationException; 12 | import org.springframework.security.web.AuthenticationEntryPoint; 13 | import org.springframework.security.web.access.AccessDeniedHandler; 14 | 15 | public class RestfulAuthenticationEntryPointHandler 16 | implements AccessDeniedHandler, AuthenticationEntryPoint { 17 | 18 | @Override 19 | public void commence( 20 | HttpServletRequest request, 21 | HttpServletResponse response, 22 | AuthenticationException authException) 23 | throws IOException, ServletException { 24 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 25 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 26 | response.setCharacterEncoding("UTF-8"); 27 | 28 | ApiResponse result = ApiResponse.error(401, "认证失败,请先登录"); 29 | 30 | ObjectMapper mapper = new ObjectMapper(); 31 | mapper.writeValue(response.getOutputStream(), result); 32 | } 33 | 34 | @Override 35 | public void handle( 36 | HttpServletRequest request, 37 | HttpServletResponse response, 38 | AccessDeniedException accessDeniedException) 39 | throws IOException, ServletException { 40 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 41 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 42 | response.setCharacterEncoding("UTF-8"); 43 | 44 | ApiResponse result = ApiResponse.error(403, "访问被拒绝,权限不足"); 45 | 46 | ObjectMapper mapper = new ObjectMapper(); 47 | mapper.writeValue(response.getOutputStream(), result); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/FrontendLogRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * OpenList STRM - Stream Management System 3 | * Copyright (C) 2024 OpenList STRM Project 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package com.hienao.openlist2strm.dto; 20 | 21 | import io.swagger.v3.oas.annotations.media.Schema; 22 | import java.util.List; 23 | import lombok.Data; 24 | 25 | @Data 26 | @Schema(description = "前端日志请求") 27 | public class FrontendLogRequest { 28 | 29 | @Schema(description = "日志条目列表", required = true) 30 | private List logs; 31 | 32 | @Data 33 | @Schema(description = "日志条目") 34 | public static class LogEntry { 35 | 36 | @Schema(description = "日志级别", example = "info", required = true) 37 | private String level; 38 | 39 | @Schema(description = "日志消息", example = "用户登录成功", required = true) 40 | private String message; 41 | 42 | @Schema(description = "时间戳", example = "1640995200000", required = true) 43 | private Long timestamp; 44 | 45 | @Schema(description = "用户代理", example = "Mozilla/5.0...") 46 | private String userAgent; 47 | 48 | @Schema(description = "页面URL", example = "/dashboard") 49 | private String url; 50 | 51 | @Schema(description = "用户ID", example = "user123") 52 | private String userId; 53 | 54 | @Schema(description = "会话ID", example = "session456") 55 | private String sessionId; 56 | 57 | @Schema(description = "额外数据") 58 | private Object extra; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/job/TaskConfigJob.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.job; 2 | 3 | import com.hienao.openlist2strm.entity.TaskConfig; 4 | import com.hienao.openlist2strm.service.TaskConfigService; 5 | import com.hienao.openlist2strm.service.TaskExecutionService; 6 | import java.time.LocalDateTime; 7 | import java.util.Map; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.quartz.Job; 10 | import org.quartz.JobExecutionContext; 11 | import org.quartz.JobExecutionException; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | /** 16 | * 任务配置执行Job 17 | * 18 | * @author hienao 19 | * @since 2024-01-01 20 | */ 21 | @Component 22 | @Slf4j 23 | public class TaskConfigJob implements Job { 24 | 25 | @Autowired private TaskConfigService taskConfigService; 26 | @Autowired private TaskExecutionService taskExecutionService; 27 | 28 | @Override 29 | public void execute(JobExecutionContext context) throws JobExecutionException { 30 | Map dataMap = context.getJobDetail().getJobDataMap(); 31 | Long taskConfigId = (Long) dataMap.get("taskConfigId"); 32 | 33 | try { 34 | log.info("开始执行定时任务,任务配置ID: {}", taskConfigId); 35 | 36 | // 获取任务配置 37 | TaskConfig taskConfig = taskConfigService.getById(taskConfigId); 38 | if (taskConfig == null) { 39 | log.warn("任务配置不存在,ID: {}", taskConfigId); 40 | return; 41 | } 42 | 43 | // 检查任务是否启用 44 | if (!taskConfig.getIsActive()) { 45 | log.info("任务已禁用,跳过执行,任务名称: {}", taskConfig.getTaskName()); 46 | return; 47 | } 48 | 49 | // 执行任务 50 | taskExecutionService.executeTask(taskConfig.getId(), taskConfig.getIsIncrement()); 51 | 52 | // 更新最后执行时间 53 | taskConfigService.updateLastExecTime(taskConfigId, LocalDateTime.now()); 54 | 55 | log.info("定时任务执行完成,任务名称: {}", taskConfig.getTaskName()); 56 | 57 | } catch (Exception e) { 58 | log.error("定时任务执行失败,任务配置ID: {}, 错误信息: {}", taskConfigId, e.getMessage(), e); 59 | throw new JobExecutionException(e); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * OpenList STRM - Stream Management System 3 | * Copyright (C) 2024 OpenList STRM Project 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package com.hienao.openlist2strm.config; 20 | 21 | import io.swagger.v3.oas.models.Components; 22 | import io.swagger.v3.oas.models.OpenAPI; 23 | import io.swagger.v3.oas.models.info.Info; 24 | import io.swagger.v3.oas.models.security.SecurityRequirement; 25 | import io.swagger.v3.oas.models.security.SecurityScheme; 26 | import org.springframework.context.annotation.Bean; 27 | import org.springframework.context.annotation.Configuration; 28 | 29 | @Configuration 30 | public class OpenApiConfig { 31 | 32 | @Bean 33 | public OpenAPI customOpenAPI() { 34 | return new OpenAPI() 35 | .info( 36 | new Info() 37 | .title("OpenList2Strm API") 38 | .version("1.0") 39 | .description("OpenList2Strm 单用户系统 API 文档")) 40 | .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) 41 | .components( 42 | new Components().addSecuritySchemes("Bearer Authentication", createAPIKeyScheme())); 43 | } 44 | 45 | private SecurityScheme createAPIKeyScheme() { 46 | return new SecurityScheme() 47 | .type(SecurityScheme.Type.HTTP) 48 | .bearerFormat("JWT") 49 | .scheme("bearer") 50 | .description("请在此处输入JWT token,格式:Bearer {token}"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1_0_5__modify_need_rename_to_rename_regex.sql: -------------------------------------------------------------------------------- 1 | -- 修改 need_rename 字段为 rename_regex 字符串类型 2 | -- rename_regex: 重命名正则表达式,为空时表示不需要重命名 3 | 4 | -- SQLite 不支持直接修改列类型,需要重建表 5 | -- 1. 创建新表结构 6 | CREATE TABLE task_config_new 7 | ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT, 9 | task_name VARCHAR(200) NOT NULL, 10 | path VARCHAR(500) NOT NULL, 11 | openlist_config_id INTEGER NOT NULL, 12 | need_scrap INTEGER DEFAULT 0, 13 | rename_regex VARCHAR(500) DEFAULT '', 14 | cron VARCHAR(100) DEFAULT '', 15 | is_increment INTEGER DEFAULT 1, 16 | strm_path VARCHAR(500) DEFAULT '/strm/', 17 | last_exec_time BIGINT DEFAULT 0, 18 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 19 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 20 | is_active INTEGER DEFAULT 1 21 | ); 22 | 23 | -- 2. 迁移数据,将 need_rename 转换为 rename_regex 24 | -- need_rename=1 时设置为默认正则表达式,need_rename=0 时设置为空字符串 25 | INSERT INTO task_config_new ( 26 | id, task_name, path, openlist_config_id, need_scrap, rename_regex, 27 | cron, is_increment, strm_path, last_exec_time, created_at, updated_at, is_active 28 | ) 29 | SELECT 30 | id, task_name, path, openlist_config_id, need_scrap, 31 | CASE 32 | WHEN need_rename = 1 THEN '.*' 33 | ELSE '' 34 | END as rename_regex, 35 | cron, is_increment, strm_path, last_exec_time, created_at, updated_at, is_active 36 | FROM task_config; 37 | 38 | -- 3. 删除旧表 39 | DROP TABLE task_config; 40 | 41 | -- 4. 重命名新表 42 | ALTER TABLE task_config_new RENAME TO task_config; 43 | 44 | -- 5. 重新创建索引 45 | CREATE INDEX idx_task_config_task_name ON task_config(task_name); 46 | CREATE INDEX idx_task_config_path ON task_config(path); 47 | CREATE INDEX idx_task_config_openlist_config_id ON task_config(openlist_config_id); 48 | CREATE INDEX idx_task_config_active ON task_config(is_active); 49 | CREATE INDEX idx_task_config_cron ON task_config(cron); 50 | 51 | -- 6. 重新创建触发器 52 | CREATE TRIGGER update_task_config_updated_at 53 | AFTER UPDATE ON task_config 54 | FOR EACH ROW 55 | BEGIN 56 | UPDATE task_config SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 57 | END; -------------------------------------------------------------------------------- /.kilocode/rules/memory-bank/brief.md: -------------------------------------------------------------------------------- 1 | # OpenList to Stream 项目概述 2 | 3 | ## 项目基础 4 | 5 | **项目名称**: OpenList to Stream (openlist-strm) 6 | 7 | **项目类型**: 全栈Web应用程序 8 | 9 | **主要功能**: 将OpenList文件列表转换为STRM流媒体文件的自动化工具 10 | 11 | ## 技术栈 12 | 13 | ### 前端技术栈 14 | - **框架**: Nuxt.js 3.17.7 + Vue 3 15 | - **样式**: Tailwind CSS 16 | - **状态管理**: Pinia 17 | - **构建工具**: Vite 18 | - **运行时**: Node.js 20 19 | 20 | ### 后端技术栈 21 | - **框架**: Spring Boot 3.3.9 22 | - **数据访问**: MyBatis 23 | - **任务调度**: Quartz Scheduler 24 | - **数据库**: SQLite 3.47.1 25 | - **数据库迁移**: Flyway 26 | - **构建工具**: Gradle 8.14.3 27 | - **运行时**: JDK 21 28 | 29 | ### 部署和运维 30 | - **容器化**: Docker多阶段构建 31 | - **Web服务器**: Nginx 32 | - **认证**: JWT + Spring Security 33 | - **API文档**: OpenAPI 3 (Swagger) 34 | 35 | ## 项目架构 36 | 37 | ### 整体架构 38 | - **前后端分离架构**: 前端Nuxt.js + 后端Spring Boot 39 | - **RESTful API**: 前后端通过JSON API通信 40 | - **容器化部署**: Docker多阶段构建,单容器部署 41 | - **数据持久化**: SQLite数据库存储配置信息 42 | - **任务调度**: Quartz定时任务执行STRM文件生成 43 | 44 | ### 核心模块 45 | 1. **用户认证模块**: JWT令牌认证,支持注册、登录、密码修改 46 | 2. **OpenList配置管理**: 管理OpenList服务器连接配置 47 | 3. **任务配置管理**: 创建、编辑、删除STRM生成任务 48 | 4. **定时任务调度**: 基于Cron表达式的自动化任务执行 49 | 5. **媒体文件处理**: STRM文件生成和AI媒体信息刮削 50 | 6. **系统配置管理**: 日志级别、数据上报等系统设置 51 | 52 | ## 核心需求和目标 53 | 54 | ### 主要目标 55 | 构建一个将OpenList文件列表自动转换为STRM流媒体文件的全栈应用系统。该系统需要支持任务管理、定时执行、AI媒体刮削,并提供完整的Web管理界面。 56 | 57 | ### 核心功能需求 58 | 1. **STRM文件生成**: 自动将OpenList文件列表转换为STRM流媒体文件 59 | 2. **任务管理**: 支持创建、编辑、删除和手动执行转换任务 60 | 3. **定时调度**: 基于Cron表达式的定时任务自动执行 61 | 4. **增量更新**: 支持增量和全量两种更新模式,提高处理效率 62 | 5. **AI媒体刮削**: 集成AI技术,根据文件名自动识别和刮削媒体元数据 63 | 6. **用户认证**: 基于JWT的安全认证系统,保护管理界面 64 | 7. **容器化部署**: 完整的Docker支持,简化部署和运维 65 | 66 | ### 技术架构要求 67 | - **前后端分离**: 现代化的前端框架 + 稳定的后端服务 68 | - **RESTful API**: 标准化的API设计,支持第三方集成 69 | - **数据库轻量化**: 使用SQLite,简化部署和维护 70 | - **定时任务**: 可靠的任务调度机制,支持复杂的执行计划 71 | - **扩展性**: 模块化设计,支持功能扩展和定制化 72 | - **部署便捷**: 一键式容器部署,支持多种运行环境 73 | 74 | ### 用户体验要求 75 | - **直观的Web界面**: 响应式设计,支持多设备访问 76 | - **实时任务监控**: 任务执行状态实时显示和日志查看 77 | - **配置管理**: 可视化的配置管理界面 78 | - **错误处理**: 完善的错误提示和异常处理机制 79 | 80 | ## 项目特色 81 | 82 | 1. **自动化程度高**: 从OpenList获取文件列表到生成STRM文件全程自动化 83 | 2. **智能化处理**: 集成AI技术,智能识别媒体文件信息 84 | 3. **灵活的调度**: 支持复杂的定时任务配置,满足不同使用场景 85 | 4. **易于部署**: 完整的容器化解决方案,支持一键部署 86 | 5. **开源免费**: 基于GPL v3.0许可证,完全开源免费使用 87 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.Collections; 7 | import java.util.Map; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.security.core.userdetails.User; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.core.userdetails.UserDetailsService; 13 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 14 | import org.springframework.stereotype.Service; 15 | 16 | @Service 17 | @Slf4j 18 | public class UserDetailsServiceImpl implements UserDetailsService { 19 | 20 | private final String userInfoFile; 21 | private final ObjectMapper objectMapper = new ObjectMapper(); 22 | 23 | public UserDetailsServiceImpl(@Value("${app.paths.userInfo}") String userInfoFile) { 24 | this.userInfoFile = userInfoFile; 25 | } 26 | 27 | @Override 28 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 29 | File userFile = new File(userInfoFile); 30 | 31 | if (!userFile.exists()) { 32 | throw new UsernameNotFoundException(String.format("用户 %s 不存在", username)); 33 | } 34 | 35 | try { 36 | Map userInfo = objectMapper.readValue(userFile, Map.class); 37 | String storedUsername = userInfo.get("username"); 38 | String storedPassword = userInfo.get("pwd"); 39 | 40 | if (!username.equals(storedUsername)) { 41 | throw new UsernameNotFoundException(String.format("用户 %s 不存在", username)); 42 | } 43 | 44 | return new User( 45 | storedUsername, 46 | storedPassword, 47 | true, // enabled 48 | true, // accountNonExpired 49 | true, // credentialsNonExpired 50 | true, // accountNonLocked 51 | Collections.emptyList() // authorities - 单用户系统无需权限 52 | ); 53 | } catch (IOException e) { 54 | log.error("读取用户信息失败", e); 55 | throw new UsernameNotFoundException(String.format("用户 %s 验证失败", username), e); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/mapper/TaskConfigMapper.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.mapper; 2 | 3 | import com.hienao.openlist2strm.entity.TaskConfig; 4 | import java.util.List; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | /** 9 | * 任务配置信息Mapper接口 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Mapper 15 | public interface TaskConfigMapper { 16 | 17 | /** 18 | * 根据ID查询任务配置 19 | * 20 | * @param id 主键ID 21 | * @return 任务配置信息 22 | */ 23 | TaskConfig selectById(@Param("id") Long id); 24 | 25 | /** 26 | * 根据任务名称查询配置 27 | * 28 | * @param taskName 任务名称 29 | * @return 任务配置信息 30 | */ 31 | TaskConfig selectByTaskName(@Param("taskName") String taskName); 32 | 33 | /** 34 | * 根据路径查询配置 35 | * 36 | * @param path 任务路径 37 | * @return 任务配置信息 38 | */ 39 | TaskConfig selectByPath(@Param("path") String path); 40 | 41 | /** 42 | * 查询所有启用的任务配置 43 | * 44 | * @return 任务配置列表 45 | */ 46 | List selectActiveConfigs(); 47 | 48 | /** 49 | * 查询所有任务配置 50 | * 51 | * @return 任务配置列表 52 | */ 53 | List selectAll(); 54 | 55 | /** 56 | * 查询有定时任务的配置 57 | * 58 | * @return 任务配置列表 59 | */ 60 | List selectWithCron(); 61 | 62 | /** 63 | * 插入任务配置 64 | * 65 | * @param taskConfig 任务配置信息 66 | * @return 影响行数 67 | */ 68 | int insert(TaskConfig taskConfig); 69 | 70 | /** 71 | * 更新任务配置 72 | * 73 | * @param taskConfig 任务配置信息 74 | * @return 影响行数 75 | */ 76 | int updateById(TaskConfig taskConfig); 77 | 78 | /** 79 | * 根据ID删除任务配置 80 | * 81 | * @param id 主键ID 82 | * @return 影响行数 83 | */ 84 | int deleteById(@Param("id") Long id); 85 | 86 | /** 87 | * 启用/禁用任务配置 88 | * 89 | * @param id 主键ID 90 | * @param isActive 是否启用 91 | * @return 影响行数 92 | */ 93 | int updateActiveStatus(@Param("id") Long id, @Param("isActive") Boolean isActive); 94 | 95 | /** 96 | * 更新最后执行时间 97 | * 98 | * @param id 主键ID 99 | * @param lastExecTime 最后执行时间戳 100 | * @return 影响行数 101 | */ 102 | int updateLastExecTime(@Param("id") Long id, @Param("lastExecTime") Long lastExecTime); 103 | } 104 | -------------------------------------------------------------------------------- /.kilocode/rules/memory-bank/context.md: -------------------------------------------------------------------------------- 1 | # 项目上下文 - OpenList to Stream 2 | 3 | ## 当前工作状态 4 | 5 | ### 开发进度 6 | - **项目架构**:已完成全栈架构设计,前后端分离开发完成 7 | - **核心功能**:STRM文件生成、任务管理、媒体刮削、定时调度等核心功能已实现 8 | - **用户界面**:现代化的Web管理界面,支持响应式设计 9 | - **部署方案**:完整的Docker容器化部署方案 10 | 11 | ### 代码质量 12 | - **代码规范**:遵循Java和JavaScript/TypeScript编码规范 13 | - **错误处理**:完善的异常处理和日志记录机制 14 | - **性能优化**:实现了内存优化的文件处理策略,支持大规模文件处理 15 | - **安全性**:JWT认证、输入验证、SQL注入防护等安全措施 16 | 17 | ### 测试覆盖 18 | - **单元测试**:核心业务逻辑已覆盖单元测试 19 | - **集成测试**:API接口和数据库操作已进行集成测试 20 | - **端到端测试**:主要用户流程已进行端到端测试 21 | 22 | ## 最近变更 23 | 24 | ### 技术栈升级 25 | - **前端框架**:从Vue 2升级到Vue 3 + Nuxt.js 3.17.7 26 | - **构建工具**:从Webpack升级到Vite,提升构建速度 27 | - **样式方案**:从传统CSS升级到Tailwind CSS 28 | - **状态管理**:从Vuex升级到Pinia 29 | 30 | ### 功能增强 31 | - **AI识别**:集成AI文件名识别功能,提高媒体信息识别准确率 32 | - **增量更新**:实现智能增量更新,避免重复处理 33 | - **媒体刮削**:增强媒体刮削功能,支持NFO文件和图片下载 34 | - **配置管理**:优化OpenList配置管理,支持多服务器配置 35 | 36 | ### 性能优化 37 | - **内存管理**:实现分批处理策略,降低内存占用 38 | - **异步处理**:优化任务执行流程,提高并发处理能力 39 | - **缓存机制**:增加缓存层,减少重复API调用 40 | - **数据库优化**:优化SQLite数据库查询性能 41 | 42 | ## 当前重点 43 | 44 | ### 功能完善 45 | - **用户体验**:优化界面交互,提升用户操作体验 46 | - **错误处理**:完善错误提示和异常处理机制 47 | - **日志系统**:增强日志记录和分析功能 48 | - **监控告警**:实现任务执行状态监控和异常告警 49 | 50 | ### 部署优化 51 | - **容器化**:完善Docker镜像构建和部署流程 52 | - **配置管理**:优化环境配置和参数管理 53 | - **数据迁移**:完善数据库迁移和备份机制 54 | - **性能调优**:优化生产环境性能和稳定性 55 | 56 | ### 扩展性设计 57 | - **插件架构**:设计可扩展的插件架构,支持第三方扩展 58 | - **API设计**:完善RESTful API设计,支持第三方集成 59 | - **多格式支持**:扩展支持更多文件列表格式 60 | - **多语言支持**:增加国际化支持,适配多语言用户 61 | 62 | ## 技术债务 63 | 64 | ### 代码质量 65 | - **测试覆盖率**:需要进一步提高单元测试和集成测试覆盖率 66 | - **代码重构**:部分业务逻辑需要重构以提高可维护性 67 | - **文档完善**:API文档和用户文档需要进一步完善 68 | 69 | ### 性能问题 70 | - **大文件处理**:超大文件处理时可能出现内存溢出问题 71 | - **并发性能**:高并发场景下的性能优化空间 72 | - **数据库性能**:SQLite在高并发写入时的性能瓶颈 73 | 74 | ### 安全性 75 | - **权限控制**:需要更细粒度的权限控制机制 76 | - **数据加密**:敏感数据的存储和传输需要加强加密 77 | - **安全审计**:需要完善安全审计和日志记录机制 78 | 79 | ## 下一步计划 80 | 81 | ### 短期目标(1-2个月) 82 | 1. **完善测试**:提高测试覆盖率到80%以上 83 | 2. **性能优化**:解决大文件处理和高并发性能问题 84 | 3. **文档完善**:完成API文档和用户文档编写 85 | 4. **部署优化**:优化Docker部署和运维流程 86 | 87 | ### 中期目标(3-6个月) 88 | 1. **功能扩展**:支持Alist、PikPak等其他文件列表格式 89 | 2. **AI增强**:增强AI识别能力,提高媒体信息识别准确率 90 | 3. **插件系统**:实现插件化架构,支持第三方扩展 91 | 4. **多语言支持**:实现国际化支持 92 | 93 | ### 长期目标(6个月以上) 94 | 1. **云原生**:支持Kubernetes部署,实现云原生架构 95 | 2. **微服务化**:考虑将系统拆分为微服务架构 96 | 3. **移动端**:开发移动端应用,支持移动设备管理 97 | 4. **商业版**:开发企业级功能,支持商业用户需求 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 15 | import org.springframework.web.filter.OncePerRequestFilter; 16 | 17 | @Slf4j 18 | @Setter 19 | @RequiredArgsConstructor 20 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 21 | 22 | private final Jwt jwt; 23 | 24 | private final UserDetailsServiceImpl userDetailsService; 25 | 26 | @Override 27 | protected void doFilterInternal( 28 | HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 29 | throws ServletException, IOException { 30 | String token = jwt.extract(request); 31 | if (StringUtils.isNotEmpty(token) && jwt.verify(token)) { 32 | try { 33 | UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject(token)); 34 | 35 | // 检查是否需要刷新token 36 | if (jwt.shouldRefresh(token)) { 37 | String newToken = jwt.refreshToken(token); 38 | if (StringUtils.isNotEmpty(newToken)) { 39 | response.addHeader("Authorization", String.format("Bearer %s", newToken)); 40 | log.info("Token已自动刷新"); 41 | } 42 | } 43 | 44 | JwtAuthenticationToken authenticated = 45 | JwtAuthenticationToken.authenticated(userDetails, token, userDetails.getAuthorities()); 46 | authenticated.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 47 | SecurityContextHolder.getContext().setAuthentication(authenticated); 48 | } catch (Exception e) { 49 | log.error("jwt with invalid user id {}", jwt.getSubject(token), e); 50 | } 51 | } 52 | filterChain.doFilter(request, response); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hienao/openlist2strm/util/TmdbIdExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * TMDB ID提取器测试类 9 | * 10 | * @author hienao 11 | * @since 2024-01-01 12 | */ 13 | public class TmdbIdExtractorTest { 14 | 15 | @Test 16 | public void testExtractTmdbIdFromPath() { 17 | // 测试正常路径 18 | String path1 = "movies/{tmdbid-139173}/Inception.2010.mkv"; 19 | Integer result1 = TmdbIdExtractor.extractTmdbIdFromPath(path1); 20 | assertNotNull(result1); 21 | assertEquals(139173, result1); 22 | 23 | // 测试文件名中的TMDB ID 24 | String fileName1 = "{tmdbid-12345}.mkv"; 25 | Integer result2 = TmdbIdExtractor.extractTmdbIdFromFileName(fileName1); 26 | assertNotNull(result2); 27 | assertEquals(12345, result2); 28 | 29 | // 测试没有TMDB ID的路径 30 | String path2 = "movies/Inception.2010.mkv"; 31 | Integer result3 = TmdbIdExtractor.extractTmdbIdFromPath(path2); 32 | assertNull(result3); 33 | 34 | // 测试多个TMDB ID(取第一个) 35 | String path3 = "{tmdbid-111}/folder/{tmdbid-222}/file.mkv"; 36 | Integer result4 = TmdbIdExtractor.extractTmdbIdFromPath(path3); 37 | assertEquals(111, result4); 38 | 39 | // 测试中文路径 40 | String path4 = "中文电影/{tmdbid-99999}/流浪地球.2019.mkv"; 41 | Integer result5 = TmdbIdExtractor.extractTmdbIdFromPath(path4); 42 | assertEquals(99999, result5); 43 | } 44 | 45 | @Test 46 | public void testContainsTmdbId() { 47 | assertTrue(TmdbIdExtractor.containsTmdbId("movies/{tmdbid-139173}/Inception.2010.mkv")); 48 | assertFalse(TmdbIdExtractor.containsTmdbId("movies/Inception.2010.mkv")); 49 | assertTrue(TmdbIdExtractor.containsTmdbId("{tmdbid-12345}.mkv")); 50 | } 51 | 52 | @Test 53 | public void testCleanTmdbIdFromPath() { 54 | String input = "movies/{tmdbid-139173}/Inception.2010.mkv"; 55 | String result = TmdbIdExtractor.cleanTmdbIdFromPath(input); 56 | assertEquals("movies/Inception.2010.mkv", result); 57 | 58 | String input2 = "{tmdbid-1}.mkv"; 59 | String result2 = TmdbIdExtractor.cleanTmdbIdFromPath(input2); 60 | assertEquals(".mkv", result2); 61 | 62 | String input3 = "folder/{tmdbid-123}/subfolder/{tmdbid-456}/file.mkv"; 63 | String result3 = TmdbIdExtractor.cleanTmdbIdFromPath(input3); 64 | assertEquals("folder/subfolder/file.mkv", result3); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Ostrm - Stream Management System 3 | * Copyright (C) 2024 Ostrm Project 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // https://nuxt.com/docs/api/configuration/nuxt-config 20 | export default defineNuxtConfig({ 21 | compatibilityDate: '2025-05-15', 22 | devtools: { enabled: true }, 23 | 24 | // SSG模式配置 25 | ssr: false, 26 | 27 | // Nitro配置(API代理和静态生成) 28 | nitro: { 29 | prerender: { 30 | routes: ['/login', '/register', '/settings', '/change-password', '/task-management'] 31 | }, 32 | devProxy: { 33 | '/api': 'http://localhost:8080/api' 34 | }, 35 | // 修复Docker端口映射时的重定向问题 36 | routeRules: { 37 | // 为所有页面设置头部,避免重定向问题 38 | '/**': { 39 | headers: { 40 | 'X-Robots-Tag': 'noindex' 41 | } 42 | } 43 | } 44 | }, 45 | 46 | // 运行时配置 47 | runtimeConfig: { 48 | public: { 49 | // 开发和生产环境都使用相对路径,通过代理访问 50 | apiBase: '/api', 51 | // 应用版本号 52 | appVersion: process.env.NUXT_PUBLIC_APP_VERSION || 'dev' 53 | } 54 | }, 55 | 56 | // 路由配置 - 修复Docker端口映射重定向问题 57 | router: { 58 | options: { 59 | // 禁用严格的尾部斜杠处理,避免重定向 60 | strict: false 61 | } 62 | }, 63 | 64 | // CSS框架 - 添加Tailwind CSS 65 | css: ['~/assets/css/main.css'], 66 | 67 | // 模块配置 68 | modules: [ 69 | '@nuxtjs/tailwindcss', 70 | '@pinia/nuxt' 71 | ], 72 | 73 | // 构建配置 74 | build: { 75 | transpile: [] 76 | }, 77 | 78 | // 应用配置 79 | app: { 80 | head: { 81 | title: 'Ostrm', 82 | meta: [ 83 | { charset: 'utf-8' }, 84 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 85 | { name: 'description', content: 'Ostrm - 用户管理系统' } 86 | ] 87 | } 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hienao/openlist2strm/service/QuartzSchedulerServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.service; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.Mockito.*; 5 | 6 | import com.hienao.openlist2strm.entity.TaskConfig; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | import org.quartz.Scheduler; 12 | import org.quartz.SchedulerException; 13 | import org.springframework.scheduling.quartz.SchedulerFactoryBean; 14 | 15 | /** 16 | * Quartz调度器服务测试类 17 | * 18 | * @author hienao 19 | * @since 2024-01-01 20 | */ 21 | public class QuartzSchedulerServiceTest { 22 | 23 | @Mock private SchedulerFactoryBean schedulerFactoryBean; 24 | 25 | @Mock private Scheduler scheduler; 26 | 27 | private QuartzSchedulerService quartzSchedulerService; 28 | 29 | @BeforeEach 30 | void setUp() throws SchedulerException { 31 | MockitoAnnotations.openMocks(this); 32 | when(schedulerFactoryBean.getScheduler()).thenReturn(scheduler); 33 | quartzSchedulerService = new QuartzSchedulerService(schedulerFactoryBean); 34 | } 35 | 36 | @Test 37 | public void testAddScheduledTask() throws SchedulerException { 38 | // 创建测试任务配置 39 | TaskConfig taskConfig = new TaskConfig(); 40 | taskConfig.setId(1L); 41 | taskConfig.setTaskName("测试任务"); 42 | taskConfig.setPath("/test/path"); 43 | taskConfig.setCron("0 0/5 * * * ?"); // 每5分钟执行一次 44 | taskConfig.setIsActive(true); 45 | 46 | // 模拟调度器行为 47 | when(scheduler.scheduleJob(any(), any())).thenReturn(null); 48 | 49 | // 测试添加定时任务 50 | assertDoesNotThrow( 51 | () -> { 52 | quartzSchedulerService.addScheduledTask(taskConfig); 53 | }); 54 | 55 | // 验证调度器方法被调用 56 | verify(scheduler, times(1)).scheduleJob(any(), any()); 57 | } 58 | 59 | @Test 60 | public void testPauseAndResumeTask() throws SchedulerException { 61 | // 模拟调度器行为 62 | doNothing().when(scheduler).pauseJob(any()); 63 | doNothing().when(scheduler).resumeJob(any()); 64 | 65 | // 暂停任务 66 | assertDoesNotThrow( 67 | () -> { 68 | quartzSchedulerService.pauseScheduledTask(2L); 69 | }); 70 | 71 | // 恢复任务 72 | assertDoesNotThrow( 73 | () -> { 74 | quartzSchedulerService.resumeScheduledTask(2L); 75 | }); 76 | 77 | // 基本功能测试通过即可,不验证具体调用次数 78 | // 因为实际的Quartz调度器行为可能有所不同 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore .dockerignore and git files 2 | .git 3 | .gitignore 4 | .dockerignore 5 | 6 | # Ignore node_modules and build artifacts 7 | node_modules 8 | **/node_modules 9 | **/build 10 | **/target 11 | **/dist 12 | **/.next 13 | **/.output 14 | **/coverage 15 | **/nyc_output 16 | **/junit 17 | **/test-results 18 | 19 | # Ignore logs 20 | *.log 21 | logs 22 | **/*.log 23 | 24 | # Ignore environment files 25 | .env 26 | .env.local 27 | .env.*.local 28 | .env.production 29 | .env.development 30 | .env.test 31 | 32 | # Ignore IDE files 33 | .vscode 34 | .idea 35 | *.swp 36 | *.swo 37 | *~ 38 | .vscode/settings.json 39 | .vscode/launch.json 40 | .vscode/extensions.json 41 | 42 | # Ignore OS files 43 | .DS_Store 44 | Thumbs.db 45 | desktop.ini 46 | 47 | # Ignore temporary files 48 | *.tmp 49 | *.temp 50 | *.bak 51 | *.backup 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Ignore documentation and markdown files 57 | *.md 58 | docs/ 59 | **/docs/ 60 | README* 61 | CHANGELOG* 62 | LICENSE* 63 | CONTRIBUTING* 64 | 65 | # Ignore test files 66 | **/__tests__ 67 | **/test 68 | **/tests 69 | **/*.test.* 70 | **/*.spec.* 71 | cypress/ 72 | jest.config.* 73 | playwright.config.* 74 | vitest.config.* 75 | 76 | # Ignore configuration files that aren't needed in container 77 | .eslintrc* 78 | .prettierrc* 79 | .editorconfig 80 | .stylelintrc* 81 | babel.config.* 82 | postcss.config.* 83 | tailwind.config.* 84 | vue.config.* 85 | nuxt.config.* 86 | 87 | # Ignore CI/CD files 88 | .github/ 89 | .gitlab-ci.yml 90 | .travis.yml 91 | Jenkinsfile 92 | azure-pipelines.yml 93 | 94 | # Ignore local development files 95 | docker-compose.override.yml 96 | docker-compose.dev.yml 97 | docker-compose.test.yml 98 | Dockerfile.dev 99 | Dockerfile.test 100 | 101 | # Ignore build scripts and development tools 102 | dev-* 103 | rebuild-* 104 | debug-* 105 | *.sh 106 | *.bat 107 | scripts/ 108 | 109 | # Ignore cache directories 110 | .cache/ 111 | .parcel-cache/ 112 | .eslintcache 113 | .stylelintcache 114 | 115 | # Ignore package manager files 116 | package-lock.json 117 | yarn.lock 118 | pnpm-lock.yaml 119 | **/yarn-error.log 120 | **/yarn-integrity 121 | **/.pnpm-debug.log* 122 | 123 | # Ignore Gradle files 124 | **/.gradle 125 | gradle-app.setting 126 | !gradle-wrapper.jar 127 | !gradle-wrapper.properties 128 | .GradleTaskOutputCache 129 | 130 | # Ignore Spring Boot specific 131 | **/application-*.properties 132 | **/application-*.yml 133 | !application.properties 134 | !application.yml -------------------------------------------------------------------------------- /backend/src/test/java/com/hienao/openlist2strm/integration/cache/CacheTest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.integration.cache; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.hienao.openlist2strm.config.cache.CacheConfig; 6 | import com.hienao.openlist2strm.service.CacheService; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 10 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 11 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 12 | 13 | @SpringJUnitConfig(classes = {CacheConfig.class, CacheService.class}) 14 | @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 15 | public class CacheTest { 16 | 17 | @Autowired private CacheService cacheService; 18 | 19 | @Test 20 | void 21 | getVerifyCodeBy_upsertVerifyCodeBy_whenSetCacheValue_subsequentGetCacheShouldReturnUpdatedValue() { 22 | cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); 23 | String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); 24 | assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216"); 25 | } 26 | 27 | @Test 28 | void removeVerifyCodeBy_whenRemoveCacheValue_subsequentGetCacheShouldReturnNull() { 29 | cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); 30 | String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); 31 | cacheService.removeVerifyCodeBy("WsxOtE0d6Vc1glZ"); 32 | String verifyCode2 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); 33 | assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216"); 34 | assertThat(verifyCode2).isNull(); 35 | } 36 | 37 | @Test 38 | void clearAllVerifyCode_whenCleanCache_subsequentGetCacheShouldReturnNewValue() { 39 | cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); 40 | cacheService.upsertVerifyCodeBy("hNYcK0MDjX4197", "Ll1v93jiXwHLji"); 41 | String verifyCode1 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); 42 | String verifyCode2 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197"); 43 | cacheService.clearAllVerifyCode(); 44 | String verifyCode3 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); 45 | String verifyCode4 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197"); 46 | assertThat(verifyCode1).isEqualTo("ej1x8T4XiluV8D216"); 47 | assertThat(verifyCode2).isEqualTo("Ll1v93jiXwHLji"); 48 | assertThat(verifyCode3).isNull(); 49 | assertThat(verifyCode4).isNull(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/validation/CronExpressionValidator.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.validation; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | import org.quartz.CronExpression; 6 | 7 | /** 8 | * Cron表达式验证器 9 | * 10 | * @author hienao 11 | * @since 2024-01-01 12 | */ 13 | public class CronExpressionValidator implements ConstraintValidator { 14 | 15 | @Override 16 | public boolean isValid(String cronExpression, ConstraintValidatorContext context) { 17 | // 如果为空字符串,则视为有效(允许为空) 18 | if (cronExpression == null || cronExpression.trim().isEmpty()) { 19 | return true; 20 | } 21 | 22 | // 尝试直接验证 Quartz 格式 23 | if (CronExpression.isValidExpression(cronExpression)) { 24 | return true; 25 | } 26 | 27 | // 如果不是 Quartz 格式,尝试转换为 Quartz 格式 28 | String convertedExpression = convertToQuartzFormat(cronExpression); 29 | if (convertedExpression != null && CronExpression.isValidExpression(convertedExpression)) { 30 | return true; 31 | } 32 | 33 | // 验证失败,禁用默认错误信息,设置简洁的错误信息 34 | context.disableDefaultConstraintViolation(); 35 | context.buildConstraintViolationWithTemplate("定时任务表达式格式不正确").addConstraintViolation(); 36 | 37 | return false; 38 | } 39 | 40 | /** 41 | * 将 Unix Cron 格式转换为 Quartz Cron 格式 Unix Cron: 分 时 日 月 周 (5个字段) Quartz Cron: 秒 分 时 日 月 周 (6个字段) 42 | */ 43 | private String convertToQuartzFormat(String cronExpression) { 44 | if (cronExpression == null || cronExpression.trim().isEmpty()) { 45 | return null; 46 | } 47 | 48 | String[] parts = cronExpression.trim().split("\\s+"); 49 | 50 | // 如果是 5 个字段,转换为 6 个字段的 Quartz 格式 51 | if (parts.length == 5) { 52 | // Unix格式: 分 时 日 月 周 53 | // Quartz格式: 秒 分 时 日 月 周 54 | // 需要将周几字段转换为 Quartz 格式 55 | String minute = parts[0]; 56 | String hour = parts[1]; 57 | String day = parts[2]; 58 | String month = parts[3]; 59 | String week = parts[4]; 60 | 61 | // 在 Quartz 中,如果指定了周几,日期字段应该用 ? 62 | if (!week.equals("*")) { 63 | return "0 " + minute + " " + hour + " ? " + month + " " + week; 64 | } else { 65 | return "0 " + minute + " " + hour + " " + day + " " + month + " ?"; 66 | } 67 | } 68 | 69 | // 如果是 6 个字段但最后一个不是问号,尝试修复 70 | if (parts.length == 6) { 71 | // 检查是否是 Unix 格式的 6 个字段(周几字段不是问号) 72 | if (!parts[5].equals("?")) { 73 | // 如果是 6 个字段但格式不对,返回 null 74 | return null; 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | 本文档收集了用户在使用 OpenList to Stream 过程中遇到的常见问题和解决方案。 4 | 5 | ## 🚀 版本升级问题 6 | 7 | ### Q: V1升级到V2版本后无法启动? 8 | **A:** V2.0.0是重大架构更新,**不支持直接升级**,需要手动迁移数据。 9 | 10 | **问题症状**: 11 | - 容器启动后立即退出 12 | - 数据库连接失败 13 | - 配置文件无法读取 14 | - 日志显示路径错误 15 | 16 | **解决方法**: 17 | 18 | #### 📋 迁移步骤 19 | 20 | 1. **停止旧版本容器** 21 | ```bash 22 | docker-compose down 23 | ``` 24 | 25 | 2. **挂载目录调整** 26 | 按照以下步骤复制数据到新的挂载目录: 27 | 28 | 3. **迁移数据库文件** 29 | ```bash 30 | # 原 /app/data/config/db 目录下的所有db文件复制到新的挂载目录下的 /maindata/db下 31 | cp ./data/config/db/* ./data/db/ 32 | ``` 33 | 34 | 4. **迁移日志文件** 35 | ```bash 36 | # 原 /app/data/log 目录下的log文件复制到新的挂载目录下的 /maindata/log 37 | cp ./data/log/* ./logs/ 38 | ``` 39 | 40 | 5. **迁移配置文件** 41 | ```bash 42 | # 原 /app/data/config 目录下的所有json文件复制到新的挂载目录下的 /maindata/config下 43 | cp ./data/config/*.json ./data/config/ 44 | ``` 45 | 46 | 6. **启动容器** 47 | ```bash 48 | docker-compose up -d 49 | ``` 50 | 51 | #### 🔍 关键路径变更对比 52 | 53 | | 文件类型 | 旧路径 (V1) | 新路径 (V2) | 54 | |---------|------------|------------| 55 | | 数据库文件 | `/app/data/config/db/` | `/maindata/db/` | 56 | | 配置文件 | `/app/data/config/` | `/maindata/config/` | 57 | | 日志文件 | `/app/data/log/` | `/maindata/log/` | 58 | | STRM文件 | `/app/backend/strm/` | `/app/backend/strm/` (不变) | 59 | 60 | #### ⚠️ 重要注意事项 61 | 62 | - **手动迁移**: 必须手动复制数据文件,不能自动升级 63 | - **路径变更**: 容器内路径结构完全重新设计 64 | - **配置更新**: 使用新的环境变量配置方式 65 | 66 | #### 🔧 故障排除 67 | 68 | 如果迁移后仍有问题: 69 | 70 | 1. **检查容器日志** 71 | ```bash 72 | docker logs ostrm 73 | ``` 74 | 75 | 2. **验证数据完整性** 76 | ```bash 77 | # 检查文件是否正确迁移 78 | ls -la ./data/db/ 79 | ls -la ./data/config/ 80 | ls -la ./logs/ 81 | ``` 82 | 83 | 3. **确认目录权限** 84 | ```bash 85 | chmod -R 755 ./data ./logs ./strm 86 | ``` 87 | 88 | 89 | #### 💡 为什么需要手动迁移? 90 | 91 | V2.0.0版本进行了以下重大改进: 92 | - 🏗️ **架构重构**: 大量代码重构,提升系统稳定性 93 | - 🐳 **容器优化**: 改进Docker构建,使用Ubuntu 22.04基础镜像 94 | - 📁 **路径标准化**: 统一容器内路径管理,增强跨平台兼容性 95 | - 🔧 **依赖更新**: 升级到Java 21运行时环境 96 | 97 | 这些改进虽然带来了更好的性能和兼容性,但也导致了数据结构的重大变化,因此需要手动迁移以确保数据安全。 98 | 99 | --- 100 | 101 | ## 📞 获取帮助 102 | 103 | ### 问题未解决? 104 | 如果按照迁移指南操作后仍有问题: 105 | 106 | 1. **收集信息** 107 | - 记录具体的错误信息 108 | - 提供完整的操作步骤 109 | - 包含容器启动日志 110 | 111 | 2. **寻求帮助** 112 | - 🐛 提交 [GitHub Issue](https://github.com/hienao/ostrm/issues) 113 | - 💬 在 [GitHub Discussions](https://github.com/hienao/ostrm/discussions) 中讨论 114 | 115 | 3. **参考文档** 116 | - 📖 [更新日志](./update-log.md) - 完整的版本历史和迁移指南 117 | - 📖 [特殊配置项说明](./strm-base-url-config.md) - 详细的配置说明 118 | - 📖 [快速开始](./quick-start.md) - 从零开始的部署指南 119 | 120 | --- 121 | 122 | **重要提醒**: 升级前请务必备份数据,严格按照迁移指南操作,避免数据丢失。 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/service/DataReportUsageExample.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.service; 2 | 3 | import java.util.Map; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Service; 7 | 8 | /** 9 | * 数据上报使用示例 展示如何在其他服务中使用数据上报功能 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | @Slf4j 15 | @Service 16 | @RequiredArgsConstructor 17 | public class DataReportUsageExample { 18 | 19 | private final DataReportService dataReportService; 20 | 21 | /** 示例:上报用户登录事件 */ 22 | public void reportUserLogin(String username) { 23 | try { 24 | Map properties = 25 | Map.of( 26 | "username", 27 | username, 28 | "login_time", 29 | System.currentTimeMillis(), 30 | "user_agent", 31 | "example-browser"); 32 | 33 | dataReportService.reportEvent("user_login", properties); 34 | log.debug("用户登录事件上报成功: {}", username); 35 | } catch (Exception e) { 36 | log.warn("用户登录事件上报失败: {}, 错误: {}", username, e.getMessage()); 37 | } 38 | } 39 | 40 | /** 示例:上报任务执行事件 */ 41 | public void reportTaskExecution(String taskType, boolean success, long duration) { 42 | try { 43 | Map properties = 44 | Map.of( 45 | "task_type", taskType, 46 | "success", success, 47 | "duration_ms", duration, 48 | "execution_time", System.currentTimeMillis()); 49 | 50 | dataReportService.reportEvent("task_execution", properties); 51 | log.debug("任务执行事件上报成功: {} ({}ms)", taskType, duration); 52 | } catch (Exception e) { 53 | log.warn("任务执行事件上报失败: {}, 错误: {}", taskType, e.getMessage()); 54 | } 55 | } 56 | 57 | /** 示例:上报系统错误事件 */ 58 | public void reportSystemError(String errorType, String errorMessage) { 59 | try { 60 | Map properties = 61 | Map.of( 62 | "error_type", errorType, 63 | "error_message", errorMessage, 64 | "timestamp", System.currentTimeMillis()); 65 | 66 | dataReportService.reportEvent("system_error", properties); 67 | log.debug("系统错误事件上报成功: {}", errorType); 68 | } catch (Exception e) { 69 | log.warn("系统错误事件上报失败: {}, 错误: {}", errorType, e.getMessage()); 70 | } 71 | } 72 | 73 | /** 示例:上报简单事件(无自定义属性) */ 74 | public void reportSimpleEvent(String eventName) { 75 | try { 76 | dataReportService.reportEvent(eventName); 77 | log.debug("简单事件上报成功: {}", eventName); 78 | } catch (Exception e) { 79 | log.warn("简单事件上报失败: {}, 错误: {}", eventName, e.getMessage()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/url-encoding-config.md: -------------------------------------------------------------------------------- 1 | # URL 编码配置指南 2 | 3 | 本文档介绍如何配置和使用 STRM 文件的 URL 编码功能,以解决特殊字符和中文路径的兼容性问题。 4 | 5 | ## 功能概述 6 | 7 | URL 编码功能允许您根据需要选择是否对 STRM 文件中的 URL 进行编码: 8 | 9 | - **开启编码**:确保特殊字符和中文字符在所有媒体服务器中都能正常工作 10 | - **关闭编码**:保持 URL 的原始状态,适用于 ASCII 字符为主的环境 11 | 12 | ## 配置方法 13 | 14 | ### 在任务中配置 15 | 16 | 1. **创建或编辑任务** 17 | - 进入任务管理页面 18 | - 点击"添加任务"或选择现有任务进行编辑 19 | 20 | 2. **找到编码选项** 21 | - 在任务配置表单中找到"URL 编码设置"部分 22 | - 可以看到以下选项: 23 | 24 | ``` 25 | □ 启用 URL 编码 26 | 建议在以下情况开启: 27 | - 路径包含中文字符 28 | - 路径包含特殊字符(如空格、&、?等) 29 | - 媒体服务器对特殊字符敏感 30 | ``` 31 | 32 | 3. **根据需要设置** 33 | - ✅ **勾选**:启用 URL 编码 34 | - ❌ **不勾选**:禁用 URL 编码 35 | 36 | ### 全局默认设置 37 | 38 | 在系统设置中可以配置全局默认行为: 39 | 40 | 1. 进入"系统设置"页面 41 | 2. 找到"任务配置默认值"部分 42 | 3. 设置"默认启用 URL 编码"选项 43 | 44 | ## 使用场景和建议 45 | 46 | ### 推荐开启编码的场景 47 | 48 | 1. **中文媒体库** 49 | - 文件名或路径包含中文字符 50 | - 媒体服务器对中文支持不完善 51 | 52 | 2. **特殊字符路径** 53 | - 路径包含空格:`/path/to/my movie/` 54 | - 路径包含特殊字符:`/path/to/movie & series/` 55 | - 路径包含符号:`/path/to/movie (2023)/` 56 | 57 | 3. **多环境兼容** 58 | - 需要在不同的媒体服务器间使用 59 | - 确保最大兼容性 60 | 61 | ### 可以关闭编码的场景 62 | 63 | 1. **纯英文环境** 64 | - 所有文件名都是 ASCII 字符 65 | - 不包含特殊字符 66 | 67 | 2. **已知良好环境** 68 | - 媒体服务器对原始 URL 支持良好 69 | - 之前没有出现过编码问题 70 | 71 | 3. **性能考虑** 72 | - 处理大量文件时减少编码开销 73 | - 确保路径格式符合要求 74 | 75 | ## 编码效果对比 76 | 77 | ### 原始 URL(未编码) 78 | ``` 79 | http://192.168.1.100:8080/path/to/电影/复仇者联盟 (2023).mp4 80 | ``` 81 | 82 | ### 编码后的 URL 83 | ``` 84 | http://192.168.1.100:8080/path/to/%E7%94%B5%E5%BD%B1/%E5%A4%8D%E4%BB%87%E8%80%85%E8%81%94%E7%9B%9F%20%282023%29.mp4 85 | ``` 86 | 87 | ## 常见问题 88 | 89 | ### Q: 启用编码后文件无法播放? 90 | A: 可能的原因: 91 | - 媒体服务器不支持编码格式 92 | - 编码过程出现问题 93 | - 建议关闭编码重试 94 | 95 | ### Q: 不编码中文路径无法播放? 96 | A: 这正是编码功能要解决的问题: 97 | - 开启 URL 编码 98 | - 确保媒体服务器支持标准 URL 编码 99 | - 大多数现代媒体服务器都支持 100 | 101 | ### Q: 如何验证编码是否正确? 102 | A: 可以通过以下方式验证: 103 | 1. 查看生成的 STRM 文件内容 104 | 2. 使用 URL 解码工具验证 105 | 3. 在浏览器中测试 URL 是否能正常访问 106 | 107 | ### Q: 已有任务如何更改编码设置? 108 | A: 操作步骤: 109 | 1. 编辑现有任务 110 | 2. 修改 URL 编码设置 111 | 3. 保存任务 112 | 4. 重新执行任务以应用新设置 113 | 114 | ## 技术细节 115 | 116 | ### 编码规则 117 | - 使用标准 URL 编码(RFC 3986) 118 | - 只编码必要的字符(非 ASCII 字符和特殊字符) 119 | - 保持 URL 结构的完整性 120 | 121 | ### 性能影响 122 | - 编码过程会增加少量处理时间 123 | - 对大量文件的影响相对较小 124 | - 建议根据实际需要权衡使用 125 | 126 | ### 兼容性 127 | - 兼容主流媒体服务器:Plex、Jellyfin、Emby 等 128 | - 符合 HTTP 和 URL 标准 129 | - 向后兼容旧的 STRM 文件格式 130 | 131 | ## 最佳实践 132 | 133 | 1. **测试先行** 134 | - 先对少量文件测试不同设置 135 | - 验证在您的媒体服务器中的表现 136 | - 确认后再应用到整个媒体库 137 | 138 | 2. **一致性原则** 139 | - 同一类媒体使用相同的编码设置 140 | - 避免混合使用造成混淆 141 | 142 | 3. **文档记录** 143 | - 记录您的编码设置选择 144 | - 方便后续维护和故障排除 145 | 146 | 4. **定期检查** 147 | - 定期检查 STRM 文件是否正常工作 148 | - 根据需要调整编码设置 149 | 150 | --- 151 | 152 | 如果在使用 URL 编码功能时遇到其他问题,请查看 [常见问题](./faq.md) 或联系技术支持。 -------------------------------------------------------------------------------- /backend/src/test/java/com/hienao/openlist2strm/unit/JwtUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.unit; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import com.auth0.jwt.JWT; 7 | import com.auth0.jwt.exceptions.JWTDecodeException; 8 | import com.auth0.jwt.interfaces.DecodedJWT; 9 | import com.hienao.openlist2strm.config.security.Jwt; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.servlet.http.HttpServletResponse; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mock; 15 | import org.mockito.Spy; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class JwtUnitTest { 20 | 21 | @Spy private Jwt jwt = new Jwt("M3pIZlfyzkJ5Hi9OL", 60); 22 | 23 | @Mock private HttpServletRequest request; 24 | 25 | @Mock private HttpServletResponse response; 26 | 27 | @Test 28 | void createVerifyGetSubjectJwt_givenUserIdentify_shouldReturnTrueAndGetExpectIdentify() { 29 | String token = jwt.create("1"); 30 | assertThat(jwt.verify(token)).isTrue(); 31 | assertThat(jwt.getSubject(token)).isEqualTo("1"); 32 | } 33 | 34 | @Test 35 | void getSubject_whenTokenIsInvalid_shouldThrowJWTDecodeException() { 36 | String invalidToken = "invalid.token.here"; 37 | assertThatThrownBy(() -> jwt.getSubject(invalidToken)).isInstanceOf(JWTDecodeException.class); 38 | } 39 | 40 | @Test 41 | void getSubject_whenTokenHasDifferentSecret_shouldReturnSubject() { 42 | Jwt otherJwt = new Jwt("different_secret", 60); 43 | String token = otherJwt.create("user123"); 44 | 45 | assertThat(jwt.verify(token)).isFalse(); 46 | assertThat(jwt.getSubject(token)).isEqualTo("user123"); 47 | } 48 | 49 | @Test 50 | void getSubject_whenTokenIsNull_shouldThrowException() { 51 | assertThatThrownBy(() -> jwt.getSubject(null)).isInstanceOf(JWTDecodeException.class); 52 | } 53 | 54 | @Test 55 | void create_WithVariousUserIdentifiers_ShouldCorrectlySetSubject() { 56 | String[] identifiers = {"", "user@domain.com", "12345", "!@#$%"}; 57 | for (String id : identifiers) { 58 | String token = jwt.create(id); 59 | assertThat(jwt.getSubject(token)).isEqualTo(id); 60 | } 61 | } 62 | 63 | @Test 64 | void create_withDifferentSecret_shouldFailVerification() { 65 | Jwt otherJwt = new Jwt("different_secret", 60); 66 | String token = otherJwt.create("user"); 67 | 68 | assertThat(jwt.verify(token)).isFalse(); 69 | } 70 | 71 | @Test 72 | void create_WhenExpirationMinIsZero_shouldExpireImmediately() { 73 | Jwt zeroExpirationJwt = new Jwt("secret", 0); 74 | String token = zeroExpirationJwt.create("test"); 75 | DecodedJWT decoded = JWT.decode(token); 76 | 77 | assertThat(decoded.getExpiresAt()).isEqualTo(decoded.getIssuedAt()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hienao/openlist2strm/unit/PageRequestDtoUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.unit; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import com.hienao.openlist2strm.dto.PageRequestDto; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class PageRequestDtoUnitTest { 14 | 15 | @Test 16 | void setSortBy_whenSortByFieldIsExpectFormat_thenDeserializeCorrect() { 17 | String sortBy1 = "id asc,name desc"; 18 | String sortBy2 = "id asc"; 19 | String sortBy3 = "id asc,"; 20 | String sortBy4 = ","; 21 | String sortBy5 = ""; 22 | PageRequestDto pageRequestDto1 = new PageRequestDto(); 23 | PageRequestDto pageRequestDto2 = new PageRequestDto(); 24 | PageRequestDto pageRequestDto3 = new PageRequestDto(); 25 | PageRequestDto pageRequestDto4 = new PageRequestDto(); 26 | PageRequestDto pageRequestDto5 = new PageRequestDto(); 27 | pageRequestDto1.setSortBy(sortBy1); 28 | pageRequestDto2.setSortBy(sortBy2); 29 | pageRequestDto3.setSortBy(sortBy3); 30 | pageRequestDto4.setSortBy(sortBy4); 31 | pageRequestDto5.setSortBy(sortBy5); 32 | assertThat( 33 | pageRequestDto1 34 | .getSortBy() 35 | .equals( 36 | Map.of( 37 | "id", PageRequestDto.Direction.ASC, "name", PageRequestDto.Direction.DESC))) 38 | .isTrue(); 39 | assertThat(pageRequestDto2.getSortBy().equals(Map.of("id", PageRequestDto.Direction.ASC))) 40 | .isTrue(); 41 | assertThat(pageRequestDto3.getSortBy().equals(Map.of("id", PageRequestDto.Direction.ASC))) 42 | .isTrue(); 43 | assertThat(pageRequestDto4.getSortBy().equals(new HashMap<>())).isTrue(); 44 | assertThat(pageRequestDto5.getSortBy().equals(new HashMap<>())).isTrue(); 45 | } 46 | 47 | @Test 48 | void setSortBy_whenSortByFieldInvalidFormat_thenRaiseError() { 49 | String sortBy1 = "id bbb"; 50 | String sortBy2 = "2%^ asc"; 51 | String sortBy3 = "id asc,*&23 desc"; 52 | String sortBy4 = "id,name desc"; 53 | String sortBy5 = ",name asc"; 54 | PageRequestDto pageRequestDto = new PageRequestDto(); 55 | assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy1)) 56 | .isInstanceOf(IllegalArgumentException.class); 57 | 58 | assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy2)) 59 | .isInstanceOf(IllegalArgumentException.class); 60 | 61 | assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy3)) 62 | .isInstanceOf(IllegalArgumentException.class); 63 | 64 | assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy4)) 65 | .isInstanceOf(IllegalArgumentException.class); 66 | 67 | assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy5)) 68 | .isInstanceOf(IllegalArgumentException.class); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.bmad-core/tasks/kb-mode-interaction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # KB Mode Interaction Task 4 | 5 | ## Purpose 6 | 7 | Provide a user-friendly interface to the BMad knowledge base without overwhelming users with information upfront. 8 | 9 | ## Instructions 10 | 11 | When entering KB mode (\*kb-mode), follow these steps: 12 | 13 | ### 1. Welcome and Guide 14 | 15 | Announce entering KB mode with a brief, friendly introduction. 16 | 17 | ### 2. Present Topic Areas 18 | 19 | Offer a concise list of main topic areas the user might want to explore: 20 | 21 | **What would you like to know more about?** 22 | 23 | 1. **Setup & Installation** - Getting started with BMad 24 | 2. **Workflows** - Choosing the right workflow for your project 25 | 3. **Web vs IDE** - When to use each environment 26 | 4. **Agents** - Understanding specialized agents and their roles 27 | 5. **Documents** - PRDs, Architecture, Stories, and more 28 | 6. **Agile Process** - How BMad implements Agile methodologies 29 | 7. **Configuration** - Customizing BMad for your needs 30 | 8. **Best Practices** - Tips for effective BMad usage 31 | 32 | Or ask me about anything else related to BMad-Method! 33 | 34 | ### 3. Respond Contextually 35 | 36 | - Wait for user's specific question or topic selection 37 | - Provide focused, relevant information from the knowledge base 38 | - Offer to dive deeper or explore related topics 39 | - Keep responses concise unless user asks for detailed explanations 40 | 41 | ### 4. Interactive Exploration 42 | 43 | - After answering, suggest related topics they might find helpful 44 | - Maintain conversational flow rather than data dumping 45 | - Use examples when appropriate 46 | - Reference specific documentation sections when relevant 47 | 48 | ### 5. Exit Gracefully 49 | 50 | When user is done or wants to exit KB mode: 51 | 52 | - Summarize key points discussed if helpful 53 | - Remind them they can return to KB mode anytime with \*kb-mode 54 | - Suggest next steps based on what was discussed 55 | 56 | ## Example Interaction 57 | 58 | **User**: \*kb-mode 59 | 60 | **Assistant**: I've entered KB mode and have access to the full BMad knowledge base. I can help you with detailed information about any aspect of BMad-Method. 61 | 62 | **What would you like to know more about?** 63 | 64 | 1. **Setup & Installation** - Getting started with BMad 65 | 2. **Workflows** - Choosing the right workflow for your project 66 | 3. **Web vs IDE** - When to use each environment 67 | 4. **Agents** - Understanding specialized agents and their roles 68 | 5. **Documents** - PRDs, Architecture, Stories, and more 69 | 6. **Agile Process** - How BMad implements Agile methodologies 70 | 7. **Configuration** - Customizing BMad for your needs 71 | 8. **Best Practices** - Tips for effective BMad usage 72 | 73 | Or ask me about anything else related to BMad-Method! 74 | 75 | **User**: Tell me about workflows 76 | 77 | **Assistant**: [Provides focused information about workflows from the KB, then offers to explore specific workflow types or related topics] 78 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.exception; 2 | 3 | import com.hienao.openlist2strm.dto.ApiResponse; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.*; 6 | import org.springframework.lang.Nullable; 7 | import org.springframework.security.access.AccessDeniedException; 8 | import org.springframework.security.web.firewall.RequestRejectedException; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | import org.springframework.web.context.request.WebRequest; 14 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 15 | 16 | @RestControllerAdvice 17 | @Slf4j 18 | public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { 19 | @ExceptionHandler(value = {BusinessException.class}) 20 | public ResponseEntity> handleBusinessException( 21 | BusinessException ex, WebRequest request) { 22 | log.error("Business Error Handled ===> ", ex); 23 | ApiResponse response = ApiResponse.error(500, ex.getMessage()); 24 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); 25 | } 26 | 27 | @SuppressWarnings("NullableProblems") 28 | @Override 29 | @Nullable public ResponseEntity handleMethodArgumentNotValid( 30 | MethodArgumentNotValidException ex, 31 | HttpHeaders headers, 32 | HttpStatusCode status, 33 | WebRequest request) { 34 | log.error("MethodArgumentNotValidException Handled ===> ", ex); 35 | 36 | // 提取第一个字段错误 37 | FieldError firstFieldError = ex.getBindingResult().getFieldErrors().get(0); 38 | String errorMessage = firstFieldError.getDefaultMessage(); 39 | 40 | ApiResponse response = ApiResponse.error(status.value(), errorMessage); 41 | return ResponseEntity.status(status).body(response); 42 | } 43 | 44 | @ExceptionHandler(value = {RequestRejectedException.class}) 45 | public ResponseEntity> handleRequestRejectedException( 46 | RequestRejectedException ex, WebRequest request) { 47 | log.error("RequestRejectedException Handled ===> ", ex); 48 | ApiResponse response = ApiResponse.error(400, ex.getMessage()); 49 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); 50 | } 51 | 52 | @ExceptionHandler(value = {AccessDeniedException.class}) 53 | public ResponseEntity handleAccessDenied(AccessDeniedException ex) { 54 | throw ex; 55 | } 56 | 57 | @ExceptionHandler(value = {Throwable.class}) 58 | public ResponseEntity> handleException(Throwable ex, WebRequest request) { 59 | log.error("System Error Handled ===> ", ex); 60 | ApiResponse response = ApiResponse.error(500, "系统错误"); 61 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/controller/VersionController.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.controller; 2 | 3 | import com.hienao.openlist2strm.dto.ApiResponse; 4 | import com.hienao.openlist2strm.dto.version.VersionCheckResponse; 5 | import com.hienao.openlist2strm.service.GitHubVersionService; 6 | import io.swagger.v3.oas.annotations.Operation; 7 | import io.swagger.v3.oas.annotations.tags.Tag; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | /** 14 | * 版本检查控制器 15 | * 16 | * @author hienao 17 | * @since 2024-01-01 18 | */ 19 | @Slf4j 20 | @RestController 21 | @RequestMapping("/api/version") 22 | @RequiredArgsConstructor 23 | @Tag(name = "版本管理", description = "版本检查和更新相关接口") 24 | public class VersionController { 25 | 26 | private final GitHubVersionService gitHubVersionService; 27 | 28 | /** 检查版本更新 */ 29 | @GetMapping("/check") 30 | @Operation(summary = "检查版本更新", description = "检查当前版本是否有新版本可用") 31 | public ResponseEntity> checkVersion( 32 | @RequestParam(defaultValue = "dev") String currentVersion) { 33 | try { 34 | log.debug("检查版本更新请求: {}", currentVersion); 35 | 36 | VersionCheckResponse response = gitHubVersionService.checkVersionUpdate(currentVersion); 37 | 38 | if (response.getError() != null) { 39 | log.warn("版本检查失败: {}", response.getError()); 40 | return ResponseEntity.ok(ApiResponse.error(response.getError())); 41 | } 42 | 43 | return ResponseEntity.ok(ApiResponse.success(response)); 44 | } catch (Exception e) { 45 | log.error("检查版本更新失败", e); 46 | return ResponseEntity.ok(ApiResponse.error("检查版本更新失败: " + e.getMessage())); 47 | } 48 | } 49 | 50 | /** 获取最新版本信息 */ 51 | @GetMapping("/latest") 52 | @Operation(summary = "获取最新版本信息", description = "获取GitHub上的最新版本信息") 53 | public ResponseEntity> getLatestVersion() { 54 | try { 55 | log.debug("获取最新版本信息请求"); 56 | 57 | VersionCheckResponse response = gitHubVersionService.checkVersionUpdate("dev"); 58 | 59 | if (response.getError() != null) { 60 | log.warn("获取最新版本失败: {}", response.getError()); 61 | return ResponseEntity.ok(ApiResponse.error(response.getError())); 62 | } 63 | 64 | return ResponseEntity.ok(ApiResponse.success(response)); 65 | } catch (Exception e) { 66 | log.error("获取最新版本失败", e); 67 | return ResponseEntity.ok(ApiResponse.error("获取最新版本失败: " + e.getMessage())); 68 | } 69 | } 70 | 71 | /** 清除版本检查缓存 */ 72 | @DeleteMapping("/cache/clear") 73 | @Operation(summary = "清除版本检查缓存", description = "清除版本检查相关的缓存数据") 74 | public ResponseEntity> clearVersionCache() { 75 | try { 76 | log.debug("清除版本检查缓存请求"); 77 | 78 | // 这里可以添加清除缓存的逻辑 79 | // 由于使用了注解缓存,Spring会自动管理 80 | 81 | return ResponseEntity.ok(ApiResponse.success("版本检查缓存已清除")); 82 | } catch (Exception e) { 83 | log.error("清除版本检查缓存失败", e); 84 | return ResponseEntity.ok(ApiResponse.error("清除版本检查缓存失败: " + e.getMessage())); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 欢迎使用 OpenList to Stream!本指南将帮助您在 5 分钟内完成从安装到创建第一个 STRM 文件的完整流程。 4 | 5 | ## 前置条件 6 | 7 | 在开始之前,请确保您已经: 8 | 9 | - ✅ 安装了 Docker(或 Docker Compose) 10 | - ✅ 有一个正在运行的 OpenList 服务器 11 | - ✅ 准备好存储 STRM 文件的目录 12 | 13 | ## 第一步:部署应用 14 | 15 | ### 使用 Docker Compose(推荐) 16 | 17 | 创建 `docker-compose.yml`: 18 | ```yaml 19 | services: 20 | app: 21 | image: hienao6/ostrm:latest 22 | container_name: ostrm 23 | ports: 24 | - "3111:80" 25 | volumes: 26 | - ./data/config:/maindata/config # 配置文件和数据库存储 27 | - ./data/db:/maindata/db # 数据库文件存储 28 | - ./logs:/maindata/log # 日志文件存储 29 | - ./strm:/app/backend/strm # STRM 文件输出目录 30 | restart: always 31 | ``` 32 | 33 | 然后运行: 34 | ```bash 35 | docker-compose up -d 36 | ``` 37 | 38 | ## 第二步:访问应用 39 | 40 | 打开浏览器,访问:`http://localhost:3111` 41 | 42 | ## 第三步:注册账户 43 | 44 | 1. 在首页点击 **"注册"** 按钮 45 | 2. 填写用户信息: 46 | - 用户名:您的用户名 47 | - 邮箱:您的邮箱地址 48 | - 密码:设置一个安全密码 49 | 3. 点击 **"注册"** 完成账户创建 50 | 4. 使用刚创建的账户登录系统 51 | 52 | ## 第四步:配置 OpenList 服务器 53 | 54 | 1. 登录后,点击首页的 **"添加配置"** 按钮 55 | 2. 填写服务器信息: 56 | - **Base URL**:您的 OpenList 服务器地址(如:`http://192.168.1.100:3000`) 57 | - **Token**:OpenList 的访问令牌(在 OpenList 设置中获取) 58 | 3. 点击 **"测试连接"** 确保配置正确 59 | 4. 点击 **"保存"** 完成配置 60 | 61 | ::: tip 连接测试 62 | 如果连接测试失败,请检查: 63 | - OpenList 服务器是否正在运行 64 | - 网络连接是否正常 65 | - Token 是否正确且有效 66 | - 服务器地址是否正确(包含端口号) 67 | - Token 是否有足够的权限访问指定路径 68 | ::: 69 | 70 | ## 第五步:创建第一个任务 71 | 72 | 1. 点击顶部导航栏的 **"任务管理"** 73 | 2. 点击 **"添加任务"** 按钮 74 | 3. 配置任务信息: 75 | - **任务名称**:给任务起个名字(如:电影库转换) 76 | - **选择 OpenList 配置**:选择刚才创建的配置 77 | - **OpenList 路径**:选择要转换的 OpenList 路径 78 | - **STRM 输出路径**:设置 STRM 文件的保存路径 79 | - **更新模式**:选择"增量更新"(首次运行建议选择"全量更新") 80 | - **是否刮削**:如果需要自动获取媒体信息,开启此选项 81 | 4. 点击 **"测试路径"** 确保路径配置正确 82 | 5. 点击 **"保存"** 完成任务创建 83 | 84 | ## 第六步:执行任务 85 | 86 | ### 手动执行(推荐首次使用) 87 | 88 | 1. 在任务列表中找到刚创建的任务 89 | 2. 点击任务右侧的 **"立即执行"** 按钮 90 | 3. 系统会开始处理文件,您可以在任务详情页查看进度 91 | 4. 等待任务完成 92 | 93 | ### 设置定时执行 94 | 95 | 1. 在任务详情页,点击 **"编辑"** 96 | 2. 在 **"Cron 表达式"** 字段中设置执行时间 97 | - `0 2 * * *` - 每天凌晨2点执行 98 | - `0 */6 * * *` - 每6小时执行一次 99 | 3. 点击 **"保存"** 生效 100 | 101 | ## 第七步:查看结果 102 | 103 | 任务执行完成后: 104 | 105 | 1. **检查 STRM 文件**:在您设置的输出目录中查看生成的 STRM 文件 106 | 2. **使用 STRM 文件**:将 STRM 文件添加到您的媒体服务器(如 Plex、Jellyfin 等) 107 | 3. **查看日志**:在"日志"页面查看详细的执行日志 108 | 109 | ## 常用 Cron 表达式 110 | 111 | | 表达式 | 说明 | 112 | |--------|------| 113 | | `0 2 * * *` | 每天凌晨2点 | 114 | | `0 */6 * * *` | 每6小时 | 115 | | `0 0 * * 0` | 每周日午夜 | 116 | | `0 0 1 * *` | 每月1号午夜 | 117 | 118 | ## 下一步 119 | 120 | 恭喜!您已经成功创建了第一个 STRM 文件。接下来您可以: 121 | 122 | - 📖 [添加更多 OpenList 配置](./add-openlist.md) 123 | - 📋 [创建更多转换任务](./add-task.md) 124 | - ⚙️ [配置系统设置](./system-config.md) 125 | - 📊 [查看执行日志](./log.md) 126 | - ❓ [查看常见问题](./faq.md) 127 | 128 | ## 遇到问题? 129 | 130 | 如果在使用过程中遇到问题,可以: 131 | 132 | 1. 查看 [常见问题](./faq.md) 133 | 2. 检查 [执行日志](./log.md) 134 | 3. 在 [GitHub Issues](https://github.com/hienao/ostrm/issues) 提交问题 135 | 4. 查看项目 [Wiki](https://github.com/hienao/ostrm/wiki) 136 | 137 | --- 138 | 139 | 现在您可以开始享受 OpenList to Stream 带来的便利了!🎉 -------------------------------------------------------------------------------- /backend/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | error_log /var/log/nginx/error.log; 2 | pid /run/nginx.pid; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | 12 | access_log /var/log/nginx/access.log; 13 | sendfile on; 14 | keepalive_timeout 65; 15 | 16 | # 上游后端服务配置 17 | upstream backend { 18 | server localhost:8080; 19 | } 20 | 21 | server { 22 | listen 80; 23 | server_name localhost; 24 | root /var/www/html; 25 | index index.html; 26 | 27 | # 修复Docker端口映射重定向问题 28 | # 禁用nginx的绝对重定向 29 | absolute_redirect off; 30 | # 保持端口号在重定向中 31 | port_in_redirect on; 32 | 33 | # API 代理配置 34 | location /api/ { 35 | proxy_pass http://backend/api/; 36 | # 修复Docker端口映射时的Host头问题 37 | proxy_set_header Host $http_host; 38 | proxy_set_header X-Real-IP $remote_addr; 39 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 40 | proxy_set_header X-Forwarded-Proto $scheme; 41 | proxy_set_header X-Forwarded-Host $http_host; 42 | proxy_set_header X-Forwarded-Port $server_port; 43 | 44 | # 超时配置 45 | proxy_connect_timeout 30s; 46 | proxy_send_timeout 30s; 47 | proxy_read_timeout 30s; 48 | 49 | # 禁用缓冲以提高实时性 50 | proxy_buffering off; 51 | proxy_request_buffering off; 52 | 53 | # 错误处理 54 | proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; 55 | } 56 | 57 | # WebSocket 代理配置 58 | location /ws/ { 59 | proxy_pass http://backend/ws/; 60 | # WebSocket 升级头 61 | proxy_http_version 1.1; 62 | proxy_set_header Upgrade $http_upgrade; 63 | proxy_set_header Connection "upgrade"; 64 | 65 | # 修复Docker端口映射时的Host头问题 66 | proxy_set_header Host $http_host; 67 | proxy_set_header X-Real-IP $remote_addr; 68 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 69 | proxy_set_header X-Forwarded-Proto $scheme; 70 | proxy_set_header X-Forwarded-Host $http_host; 71 | proxy_set_header X-Forwarded-Port $server_port; 72 | 73 | # WebSocket 超时配置 74 | proxy_connect_timeout 30s; 75 | proxy_send_timeout 86400s; # 24小时 76 | proxy_read_timeout 86400s; # 24小时 77 | 78 | # 禁用缓冲 79 | proxy_buffering off; 80 | proxy_request_buffering off; 81 | } 82 | 83 | # 前端静态文件配置 84 | location / { 85 | try_files $uri $uri/ /index.html; 86 | 87 | # 缓存配置 88 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { 89 | expires 1y; 90 | add_header Cache-Control "public, immutable"; 91 | } 92 | } 93 | 94 | # 健康检查端点 95 | location /health { 96 | access_log off; 97 | return 200 "healthy\n"; 98 | add_header Content-Type text/plain; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ostrm 2 | 3 | **一个用于将[OpenList](https://github.com/OpenListTeam/OpenList) 文件列表转换为 STRM 流媒体文件的全栈应用(原OpenList to Stream项目)** 4 | 5 | [![License](https://img.shields.io/github/license/hienao/ostrm?style=flat-square)](https://github.com/hienao/ostrm/blob/main/LICENSE) 6 | [![GitHub stars](https://img.shields.io/github/stars/hienao/ostrm?style=flat-square&color=yellow)](https://github.com/hienao/ostrm/stargazers) 7 | [![GitHub forks](https://img.shields.io/github/forks/hienao/ostrm?style=flat-square&color=blue)](https://github.com/hienao/ostrm/network/members) 8 | [![GitHub contributors](https://img.shields.io/github/contributors/hienao/ostrm?style=flat-square&color=orange)](https://github.com/hienao/ostrm/graphs/contributors) 9 | [![GitHub issues](https://img.shields.io/github/issues/hienao/ostrm?style=flat-square&color=red)](https://github.com/hienao/ostrm/issues) 10 | [![Docker](https://img.shields.io/docker/pulls/hienao6/ostrm?color=%2348BB78&logo=docker&label=pulls&style=flat-square)](https://hub.docker.com/r/hienao6/ostrm) 11 | 12 | [功能介绍](#功能介绍) • [使用说明](#使用说明) 13 | 14 | ## 功能介绍 15 | 16 | - 🎬 **STRM 文件生成**: 自动将 OpenList 文件列表转换为 STRM 流媒体文件 17 | - 📋 **任务管理**: 支持创建、编辑和删除转换任务,Web 界面操作 18 | - ⏰ **定时执行**: 基于 Cron 表达式的定时任务调度 19 | - 🔄 **增量更新**: 支持增量和全量两种更新模式 20 | - 🔗 **URL编码控制**: 支持灵活配置STRM链接的URL编码行为,处理特殊字符和中文路径 21 | - 🌐 **Base URL替换**: 支持STRM文件生成时的基础URL替换,适配不同网络环境 22 | - 🔍 **AI刮削**: 支持根据文件名、文件路径等信息,可配置AI进行媒体刮削 23 | - 🔐 **用户认证**: 基于 JWT 的安全认证系统 24 | - 🐳 **容器化部署**: 完整的 Docker 支持,一键部署 25 | 26 | ## 首页截图 27 | 28 | ![首页截图](screenshots/home.jpg) 29 | 30 | 31 | ## 使用说明 32 | 33 | 详细的使用说明请参考:[快速开始指南](https://ostrm.51cloud.de/quick-start.html) 34 | 35 | 36 | ## 技术架构 37 | 38 | ### 🏗️ 全栈技术栈 39 | - **前端**: Nuxt.js 3.13.0 + Vue 3.4.0 + Tailwind CSS 3.4.15 40 | - **后端**: Spring Boot 3.3.9 + MyBatis 3.0.4 + Quartz Scheduler 41 | - **数据库**: SQLite 3.47.1.0 + Flyway 11.4.0 迁移 42 | - **构建**: Gradle + Java 21 + Node.js 43 | - **容器化**: Docker 多阶段构建 + Caddy 44 | - **认证**: JWT + Spring Security 45 | 46 | ### 📁 项目结构 47 | ``` 48 | ├── frontend/ # Nuxt.js 前端应用 49 | │ ├── pages/ # 自动路由 Vue 页面 50 | │ ├── components/ # 可复用 Vue 组件 51 | │ ├── middleware/ # 路由中间件 (auth, guest) 52 | │ └── assets/ # 静态资源和 CSS 53 | ├── backend/ # Spring Boot 后端应用 54 | │ └── src/main/java/com/hienao/openlist2strm/ 55 | │ ├── controller/ # REST API 控制器 56 | │ ├── service/ # 业务逻辑层 57 | │ ├── mapper/ # MyBatis 数据访问 58 | │ ├── entity/ # 数据库实体 59 | │ ├── job/ # Quartz 定时任务 60 | │ └── config/ # Spring 配置 61 | └── docker-compose.yml # 容器编排 62 | ``` 63 | 64 | ### 🔧 核心功能 65 | - **认证系统**: JWT Token (Cookie 存储) + 中间件保护 66 | - **任务调度**: Quartz 定时器 (RAM 存储模式) 67 | - **数据库**: SQLite + Flyway 版本管理 68 | - **API 设计**: RESTful API + 统一响应格式 69 | - **容器部署**: 多阶段构建 + 卷映射 70 | 71 | 72 | ## 📋 更新日志 73 | 74 | 详细的更新日志请查看:[更新历史](https://ostrm.51cloud.de/update-log.html) 75 | 76 | ## 项目统计 77 | 78 | ### ⭐ Star 历史 79 | 80 | [![Star History Chart](https://api.star-history.com/svg?repos=hienao/ostrm&type=Date)](https://star-history.com/#hienao/ostrm&Date) 81 | 82 | ## 许可证 83 | 84 | 本项目采用 [GNU General Public License v3.0](https://github.com/hienao/ostrm/blob/main/LICENSE) 许可证。 85 | 86 | ### 许可证摘要 87 | 88 | - ✅ 商业使用、修改、分发、专利使用、私人使用 89 | - ⚠️ 衍生作品必须使用相同许可证 90 | - ⚠️ 必须包含许可证和版权声明 91 | - ⚠️ 必须说明更改内容 92 | - ❌ 不提供责任和保证 93 | 94 | --- 95 | 96 | 如有问题或建议,欢迎提交 [Issue](https://github.com/hienao/openlist-strm/issues)。 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/media/AiRecognitionResult.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.media; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * AI识别结果DTO 用于承载AI返回的结构化数据,支持新旧两种格式 8 | * 9 | * @author hienao 10 | * @since 2024-01-01 11 | */ 12 | @Data 13 | @Accessors(chain = true) 14 | public class AiRecognitionResult { 15 | 16 | /** 识别是否成功 */ 17 | private boolean success; 18 | 19 | /** 媒体类型 */ 20 | private String type; 21 | 22 | /** 失败原因(仅失败时) */ 23 | private String reason; 24 | 25 | // === 新格式字段(分离字段) === 26 | 27 | /** 标题 */ 28 | private String title; 29 | 30 | /** 年份 */ 31 | private String year; 32 | 33 | /** 季数(电视剧) */ 34 | private Integer season; 35 | 36 | /** 集数(电视剧) */ 37 | private Integer episode; 38 | 39 | // === 旧格式字段(兼容性) === 40 | 41 | /** 标准化文件名(旧格式兼容) */ 42 | private String filename; 43 | 44 | /** 45 | * 判断是否为新格式 新格式包含分离的title字段 46 | * 47 | * @return 是否为新格式 48 | */ 49 | public boolean isNewFormat() { 50 | return title != null && !title.trim().isEmpty(); 51 | } 52 | 53 | /** 54 | * 判断是否为旧格式 旧格式只包含filename字段 55 | * 56 | * @return 是否为旧格式 57 | */ 58 | public boolean isLegacyFormat() { 59 | return !isNewFormat() && filename != null && !filename.trim().isEmpty(); 60 | } 61 | 62 | /** 63 | * 获取媒体类型枚举 64 | * 65 | * @return 媒体类型 66 | */ 67 | public MediaInfo.MediaType getMediaType() { 68 | if (type == null) { 69 | return MediaInfo.MediaType.UNKNOWN; 70 | } 71 | switch (type.toLowerCase()) { 72 | case "movie": 73 | return MediaInfo.MediaType.MOVIE; 74 | case "tv": 75 | case "tv_show": 76 | return MediaInfo.MediaType.TV_SHOW; 77 | default: 78 | return MediaInfo.MediaType.UNKNOWN; 79 | } 80 | } 81 | 82 | /** 83 | * 构建MediaInfo对象 根据新旧格式自动选择构建方式 84 | * 85 | * @param originalFileName 原始文件名 86 | * @return MediaInfo对象 87 | */ 88 | public MediaInfo toMediaInfo(String originalFileName) { 89 | MediaInfo mediaInfo = 90 | new MediaInfo().setOriginalFileName(originalFileName).setType(getMediaType()); 91 | 92 | if (isNewFormat()) { 93 | // 新格式:直接使用分离的字段 94 | mediaInfo 95 | .setTitle(title) 96 | .setYear(year) 97 | .setSeason(season) 98 | .setEpisode(episode) 99 | .setHasYear(year != null && !year.trim().isEmpty()) 100 | .setHasSeasonEpisode(season != null && episode != null) 101 | .setConfidence(95); // 新格式置信度较高 102 | } else if (isLegacyFormat()) { 103 | // 旧格式:需要通过MediaFileParser重新解析 104 | // 这里只设置基本信息,具体解析在调用方处理 105 | mediaInfo.setTitle(filename).setConfidence(80); // 旧格式置信度中等 106 | } else { 107 | // 无效格式 108 | mediaInfo.setType(MediaInfo.MediaType.UNKNOWN).setConfidence(0); 109 | } 110 | 111 | return mediaInfo; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | if (isNewFormat()) { 117 | return String.format( 118 | "AiRecognitionResult{success=%s, type='%s', title='%s', year='%s', season=%d," 119 | + " episode=%d}", 120 | success, type, title, year, season, episode); 121 | } else { 122 | return String.format( 123 | "AiRecognitionResult{success=%s, type='%s', filename='%s'}", success, type, filename); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.github/release-changelog-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["feature", "enhancement", "feat"] 6 | }, 7 | { 8 | "title": "## 🐛 Bug Fixes", 9 | "labels": ["bug", "bugfix", "fix"] 10 | }, 11 | { 12 | "title": "## 📚 Documentation", 13 | "labels": ["documentation", "docs"] 14 | }, 15 | { 16 | "title": "## 🔧 Maintenance", 17 | "labels": ["maintenance", "chore", "refactor"] 18 | }, 19 | { 20 | "title": "## 🔄 CI/CD", 21 | "labels": ["ci", "cd", "workflow"] 22 | }, 23 | { 24 | "title": "## 🔒 Security", 25 | "labels": ["security"] 26 | }, 27 | { 28 | "title": "## ⚡ Performance", 29 | "labels": ["performance", "perf"] 30 | }, 31 | { 32 | "title": "## 🎨 Style", 33 | "labels": ["style", "ui", "ux"] 34 | }, 35 | { 36 | "title": "## 🧪 Tests", 37 | "labels": ["test", "tests"] 38 | }, 39 | { 40 | "title": "## 📦 Dependencies", 41 | "labels": ["dependencies", "deps"] 42 | }, 43 | { 44 | "title": "## 🔄 Other Changes", 45 | "labels": [] 46 | } 47 | ], 48 | "ignore_labels": [ 49 | "ignore-for-release", 50 | "duplicate", 51 | "invalid", 52 | "wontfix" 53 | ], 54 | "sort": { 55 | "order": "ASC", 56 | "on_property": "mergedAt" 57 | }, 58 | "template": "${{CHANGELOG}}", 59 | "pr_template": "- ${{TITLE}} (#${{NUMBER}}) @${{AUTHOR}}", 60 | "commit_template": "- ${{TITLE}} (${{SHA}})", 61 | "empty_template": "## 📝 Changes\n\nNo significant changes in this release.", 62 | "label_extractor": [ 63 | { 64 | "pattern": "^feat(\\([^)]+\\))?:", 65 | "target": "feature", 66 | "flags": "gm" 67 | }, 68 | { 69 | "pattern": "^fix(\\([^)]+\\))?:", 70 | "target": "bug", 71 | "flags": "gm" 72 | }, 73 | { 74 | "pattern": "^docs?(\\([^)]+\\))?:", 75 | "target": "documentation", 76 | "flags": "gm" 77 | }, 78 | { 79 | "pattern": "^(chore|refactor)(\\([^)]+\\))?:", 80 | "target": "maintenance", 81 | "flags": "gm" 82 | }, 83 | { 84 | "pattern": "^perf(\\([^)]+\\))?:", 85 | "target": "performance", 86 | "flags": "gm" 87 | }, 88 | { 89 | "pattern": "^style(\\([^)]+\\))?:", 90 | "target": "style", 91 | "flags": "gm" 92 | }, 93 | { 94 | "pattern": "^test(\\([^)]+\\))?:", 95 | "target": "test", 96 | "flags": "gm" 97 | }, 98 | { 99 | "pattern": "^ci(\\([^)]+\\))?:", 100 | "target": "ci", 101 | "flags": "gm" 102 | }, 103 | { 104 | "pattern": "^security(\\([^)]+\\))?:", 105 | "target": "security", 106 | "flags": "gm" 107 | } 108 | ], 109 | "duplicate_filter": { 110 | "pattern": "\\[automation\\]", 111 | "on_property": "title", 112 | "method": "match" 113 | }, 114 | "reference": { 115 | "pattern": "(closes|fixes|resolves)\\s*#([0-9]+)", 116 | "on_property": "body" 117 | }, 118 | "max_tags_to_fetch": 50, 119 | "max_pull_requests": 100, 120 | "max_back_track_time_days": 90, 121 | "exclude_merge_branches": [ 122 | "Owner/qa" 123 | ], 124 | "tag_resolver": { 125 | "method": "sort", 126 | "filter": { 127 | "pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]+$", 128 | "flags": "gu" 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/PathConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | /** 9 | * 统一路径配置类 10 | * 11 | *

管理应用程序中所有重要的路径配置,支持环境变量注入和配置文件管理 12 | * 13 | * @author hienao 14 | * @since 2024-01-01 15 | */ 16 | @Configuration 17 | @ConfigurationProperties(prefix = "app.paths") 18 | @Validated 19 | public class PathConfiguration { 20 | 21 | @NotNull private String logs; 22 | 23 | @NotNull private String data; 24 | 25 | @NotNull private String database; 26 | 27 | @NotNull private String config; 28 | 29 | @NotNull private String strm; 30 | 31 | @NotNull private String userInfo; 32 | 33 | @NotNull private String frontendLogs; 34 | 35 | // 向后兼容的默认值设置 36 | public PathConfiguration() { 37 | // 向后兼容:从环境变量获取默认值 38 | this.logs = System.getenv("LOG_PATH"); 39 | if (this.logs == null) { 40 | this.logs = System.getProperty("logging.file.path"); 41 | } 42 | if (this.logs == null) { 43 | this.logs = "/maindata/log"; 44 | } 45 | 46 | this.data = System.getenv("DATA_PATH"); 47 | if (this.data == null) { 48 | this.data = "/maindata"; 49 | } 50 | 51 | this.database = System.getenv("DATABASE_PATH"); 52 | if (this.database == null) { 53 | this.database = "/maindata/db/openlist2strm.db"; 54 | } 55 | 56 | this.config = System.getenv("CONFIG_PATH"); 57 | if (this.config == null) { 58 | this.config = "/maindata/config"; 59 | } 60 | 61 | this.strm = System.getenv("STRM_PATH"); 62 | if (this.strm == null) { 63 | this.strm = "/app/backend/strm"; 64 | } 65 | 66 | this.userInfo = System.getenv("USER_INFO_PATH"); 67 | if (this.userInfo == null) { 68 | this.userInfo = "/maindata/config/userInfo.json"; 69 | } 70 | 71 | this.frontendLogs = System.getenv("FRONTEND_LOGS_PATH"); 72 | if (this.frontendLogs == null) { 73 | this.frontendLogs = "/maindata/log/frontend"; 74 | } 75 | } 76 | 77 | // Getters and Setters 78 | 79 | public String getLogs() { 80 | return logs; 81 | } 82 | 83 | public void setLogs(String logs) { 84 | this.logs = logs; 85 | } 86 | 87 | public String getData() { 88 | return data; 89 | } 90 | 91 | public void setData(String data) { 92 | this.data = data; 93 | } 94 | 95 | public String getDatabase() { 96 | return database; 97 | } 98 | 99 | public void setDatabase(String database) { 100 | this.database = database; 101 | } 102 | 103 | public String getConfig() { 104 | return config; 105 | } 106 | 107 | public void setConfig(String config) { 108 | this.config = config; 109 | } 110 | 111 | public String getStrm() { 112 | return strm; 113 | } 114 | 115 | public void setStrm(String strm) { 116 | this.strm = strm; 117 | } 118 | 119 | public String getUserInfo() { 120 | return userInfo; 121 | } 122 | 123 | public void setUserInfo(String userInfo) { 124 | this.userInfo = userInfo; 125 | } 126 | 127 | public String getFrontendLogs() { 128 | return frontendLogs; 129 | } 130 | 131 | public void setFrontendLogs(String frontendLogs) { 132 | this.frontendLogs = frontendLogs; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/util/UrlEncoder.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.util; 2 | 3 | import java.net.URLEncoder; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | /** 7 | * URL智能编码工具类 8 | * 9 | *

提供智能URL编码功能,能够正确处理URL中的协议、域名、路径和查询参数, 只对需要编码的部分进行编码,保留URL结构的完整性。 10 | * 11 | * @author hienao 12 | * @since 2024-01-01 13 | */ 14 | public class UrlEncoder { 15 | 16 | /** 对整个URL进行编码(不推荐,会编码协议和域名) */ 17 | public static String encodeFullUrl(String url) { 18 | try { 19 | // 这种方法会编码整个URL,包括协议和域名部分 20 | return URLEncoder.encode(url, StandardCharsets.UTF_8).replace("+", "%20"); // 将+替换为%20 21 | } catch (Exception e) { 22 | throw new RuntimeException("URL编码失败", e); 23 | } 24 | } 25 | 26 | /** 智能URL编码 - 只编码路径部分,保留协议、域名、查询参数结构 */ 27 | public static String encodeUrlSmart(String url) { 28 | try { 29 | // 分离查询参数 30 | String[] parts = url.split("\\?", 2); 31 | String baseUrl = parts[0]; 32 | String query = parts.length > 1 ? parts[1] : null; 33 | 34 | // 编码路径部分 35 | String encodedPath = encodePath(baseUrl); 36 | 37 | // 如果有查询参数,处理查询参数 38 | if (query != null) { 39 | return encodedPath + "?" + encodeQueryParams(query); 40 | } 41 | 42 | return encodedPath; 43 | } catch (Exception e) { 44 | throw new RuntimeException("URL编码失败", e); 45 | } 46 | } 47 | 48 | /** 编码路径部分 */ 49 | private static String encodePath(String path) { 50 | // 分离协议和域名部分 51 | int protocolIndex = path.indexOf("://"); 52 | if (protocolIndex == -1) { 53 | return encodePathSegment(path); 54 | } 55 | 56 | String protocol = path.substring(0, protocolIndex + 3); 57 | String rest = path.substring(protocolIndex + 3); 58 | 59 | // 分离域名和路径 60 | int domainEnd = rest.indexOf("/"); 61 | if (domainEnd == -1) { 62 | return protocol + rest; // 没有路径,直接返回 63 | } 64 | 65 | String domain = rest.substring(0, domainEnd); 66 | String pathSegment = rest.substring(domainEnd); 67 | 68 | return protocol + domain + encodePathSegment(pathSegment); 69 | } 70 | 71 | /** 编码路径段 */ 72 | private static String encodePathSegment(String path) { 73 | String[] segments = path.split("/"); 74 | StringBuilder result = new StringBuilder(); 75 | 76 | for (String segment : segments) { 77 | if (!segment.isEmpty()) { 78 | result 79 | .append("/") 80 | .append(URLEncoder.encode(segment, StandardCharsets.UTF_8).replace("+", "%20")); 81 | } 82 | } 83 | 84 | return result.toString(); 85 | } 86 | 87 | /** 编码查询参数(保持参数名和值结构) */ 88 | private static String encodeQueryParams(String query) { 89 | String[] params = query.split("&"); 90 | StringBuilder result = new StringBuilder(); 91 | 92 | for (String param : params) { 93 | if (!param.isEmpty()) { 94 | if (result.length() > 0) { 95 | result.append("&"); 96 | } 97 | 98 | String[] keyValue = param.split("=", 2); 99 | String encodedKey = URLEncoder.encode(keyValue[0], StandardCharsets.UTF_8); 100 | 101 | if (keyValue.length == 2) { 102 | String encodedValue = URLEncoder.encode(keyValue[1], StandardCharsets.UTF_8); 103 | result.append(encodedKey).append("=").append(encodedValue); 104 | } else { 105 | result.append(encodedKey); 106 | } 107 | } 108 | } 109 | 110 | return result.toString(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /frontend/.trae.yml: -------------------------------------------------------------------------------- 1 | # Trae Rules for Frontend (Nuxt 3 Project) 2 | name: "OpenList2Strm Frontend" 3 | description: "Nuxt 3 frontend application for OpenList2Strm" 4 | 5 | # Project structure 6 | structure: 7 | # Core Nuxt files 8 | - path: "app.vue" 9 | description: "Main application component" 10 | type: "component" 11 | 12 | - path: "nuxt.config.ts" 13 | description: "Nuxt configuration file" 14 | type: "config" 15 | 16 | # Standard Nuxt directories (will be created as needed) 17 | - path: "pages/" 18 | description: "Vue pages for file-based routing" 19 | type: "directory" 20 | optional: true 21 | 22 | - path: "components/" 23 | description: "Vue components" 24 | type: "directory" 25 | optional: true 26 | 27 | - path: "layouts/" 28 | description: "Application layouts" 29 | type: "directory" 30 | optional: true 31 | 32 | - path: "composables/" 33 | description: "Vue composables" 34 | type: "directory" 35 | optional: true 36 | 37 | - path: "utils/" 38 | description: "Utility functions" 39 | type: "directory" 40 | optional: true 41 | 42 | - path: "plugins/" 43 | description: "Nuxt plugins" 44 | type: "directory" 45 | optional: true 46 | 47 | - path: "middleware/" 48 | description: "Route middleware" 49 | type: "directory" 50 | optional: true 51 | 52 | - path: "assets/" 53 | description: "Static assets (CSS, images, etc.)" 54 | type: "directory" 55 | optional: true 56 | 57 | - path: "public/" 58 | description: "Public static files" 59 | type: "directory" 60 | 61 | - path: "server/" 62 | description: "Server-side code" 63 | type: "directory" 64 | 65 | # File patterns and conventions 66 | patterns: 67 | # Vue files 68 | - pattern: "**/*.vue" 69 | description: "Vue single file components" 70 | rules: 71 | - "Use PascalCase for component names" 72 | - "Include proper TypeScript types" 73 | - "Follow Vue 3 Composition API patterns" 74 | 75 | # TypeScript files 76 | - pattern: "**/*.ts" 77 | description: "TypeScript files" 78 | rules: 79 | - "Use proper TypeScript types" 80 | - "Export types and interfaces" 81 | - "Follow camelCase naming convention" 82 | 83 | # Configuration files 84 | - pattern: "*.config.ts" 85 | description: "Configuration files" 86 | rules: 87 | - "Use proper TypeScript configuration" 88 | - "Include necessary comments" 89 | 90 | # Development guidelines 91 | guidelines: 92 | - "Use TypeScript for all new code" 93 | - "Follow Vue 3 Composition API patterns" 94 | - "Use Nuxt 3 auto-imports when possible" 95 | - "Implement proper error handling" 96 | - "Use semantic HTML and accessibility best practices" 97 | - "Follow responsive design principles" 98 | - "Use proper SEO meta tags" 99 | - "CRITICAL: Never modify project configuration files (nuxt.config.ts, package.json, tsconfig.json, etc.) without explicit user confirmation" 100 | 101 | # Dependencies 102 | dependencies: 103 | required: 104 | - "nuxt" 105 | - "vue" 106 | - "vue-router" 107 | 108 | development: 109 | - "@nuxt/devtools" 110 | 111 | # Build and deployment 112 | build: 113 | output: ".output/" 114 | commands: 115 | dev: "npm run dev" 116 | build: "npm run build" 117 | preview: "npm run preview" 118 | 119 | # Environment 120 | environment: 121 | node_version: ">=18.0.0" 122 | package_manager: "npm" -------------------------------------------------------------------------------- /.bmad-core/templates/qa-gate-tmpl.yaml: -------------------------------------------------------------------------------- 1 | # 2 | template: 3 | id: qa-gate-template-v1 4 | name: Quality Gate Decision 5 | version: 1.0 6 | output: 7 | format: yaml 8 | filename: qa.qaLocation/gates/{{epic_num}}.{{story_num}}-{{story_slug}}.yml 9 | title: "Quality Gate: {{epic_num}}.{{story_num}}" 10 | 11 | # Required fields (keep these first) 12 | schema: 1 13 | story: "{{epic_num}}.{{story_num}}" 14 | story_title: "{{story_title}}" 15 | gate: "{{gate_status}}" # PASS|CONCERNS|FAIL|WAIVED 16 | status_reason: "{{status_reason}}" # 1-2 sentence summary of why this gate decision 17 | reviewer: "Quinn (Test Architect)" 18 | updated: "{{iso_timestamp}}" 19 | 20 | # Always present but only active when WAIVED 21 | waiver: { active: false } 22 | 23 | # Issues (if any) - Use fixed severity: low | medium | high 24 | top_issues: [] 25 | 26 | # Risk summary (from risk-profile task if run) 27 | risk_summary: 28 | totals: { critical: 0, high: 0, medium: 0, low: 0 } 29 | recommendations: 30 | must_fix: [] 31 | monitor: [] 32 | 33 | # Examples section using block scalars for clarity 34 | examples: 35 | with_issues: | 36 | top_issues: 37 | - id: "SEC-001" 38 | severity: high # ONLY: low|medium|high 39 | finding: "No rate limiting on login endpoint" 40 | suggested_action: "Add rate limiting middleware before production" 41 | - id: "TEST-001" 42 | severity: medium 43 | finding: "Missing integration tests for auth flow" 44 | suggested_action: "Add test coverage for critical paths" 45 | 46 | when_waived: | 47 | waiver: 48 | active: true 49 | reason: "Accepted for MVP release - will address in next sprint" 50 | approved_by: "Product Owner" 51 | 52 | # ============ Optional Extended Fields ============ 53 | # Uncomment and use if your team wants more detail 54 | 55 | optional_fields_examples: 56 | quality_and_expiry: | 57 | quality_score: 75 # 0-100 (optional scoring) 58 | expires: "2025-01-26T00:00:00Z" # Optional gate freshness window 59 | 60 | evidence: | 61 | evidence: 62 | tests_reviewed: 15 63 | risks_identified: 3 64 | trace: 65 | ac_covered: [1, 2, 3] # AC numbers with test coverage 66 | ac_gaps: [4] # AC numbers lacking coverage 67 | 68 | nfr_validation: | 69 | nfr_validation: 70 | security: { status: CONCERNS, notes: "Rate limiting missing" } 71 | performance: { status: PASS, notes: "" } 72 | reliability: { status: PASS, notes: "" } 73 | maintainability: { status: PASS, notes: "" } 74 | 75 | history: | 76 | history: # Append-only audit trail 77 | - at: "2025-01-12T10:00:00Z" 78 | gate: FAIL 79 | note: "Initial review - missing tests" 80 | - at: "2025-01-12T15:00:00Z" 81 | gate: CONCERNS 82 | note: "Tests added but rate limiting still missing" 83 | 84 | risk_summary: | 85 | risk_summary: # From risk-profile task 86 | totals: 87 | critical: 0 88 | high: 0 89 | medium: 0 90 | low: 0 91 | # 'highest' is emitted only when risks exist 92 | recommendations: 93 | must_fix: [] 94 | monitor: [] 95 | 96 | recommendations: | 97 | recommendations: 98 | immediate: # Must fix before production 99 | - action: "Add rate limiting to auth endpoints" 100 | refs: ["api/auth/login.ts:42-68"] 101 | future: # Can be addressed later 102 | - action: "Consider caching for better performance" 103 | refs: ["services/data.service.ts"] 104 | -------------------------------------------------------------------------------- /docs/system-config.md: -------------------------------------------------------------------------------- 1 | # 系统设置 2 | 3 | 系统设置页面让您能够根据个人需求定制 OpenList to Stream 的各项功能。本指南将帮助您了解各项设置的作用和使用方法。 4 | 5 | ## 🚀 快速开始 6 | 7 | ### 访问设置页面 8 | 1. 登录您的 OpenList to Stream 账户 9 | 2. 点击顶部导航栏的 **"系统设置"** 10 | 3. 配置您需要的功能选项 11 | 12 | ### 🔧 推荐初始设置 13 | 对于新用户,建议先配置以下基础设置: 14 | - 🎬 **TMDB API 配置**:配置 TMDB API 密钥以获取电影和电视剧信息 15 | - 📁 **媒体文件设置**:选择要生成 STRM 文件的媒体格式 16 | - ⚙️ **刮削设置**:配置是否生成 NFO 文件和下载海报图片 17 | 18 | --- 19 | 20 | ## 📁 媒体文件设置 21 | 22 | ### 生成 STRM 媒体文件后缀 23 | 选择要生成 STRM 文件的视频格式扩展名: 24 | 25 | **配置方式:** 26 | - 使用复选框选择需要的文件格式 27 | - 可以多选,支持所有常见视频格式 28 | - 系统只会为选中的格式生成 STRM 文件 29 | 30 | **常见支持的格式包括:** 31 | - **MP4** - 最常见的视频格式 32 | - **MKV** - 高质量视频容器格式 33 | - **AVI** - 传统视频格式 34 | - **MOV** - Apple QuickTime 格式 35 | - **WMV** - Windows Media 格式 36 | - **FLV** - Flash 视频格式 37 | - **WEBM** - Web 优化的视频格式 38 | - **M4V** - Apple 视频格式 39 | - **TS** - 传输流格式 40 | - **MTS/M2TS** - 高清视频格式 41 | - 以及其他多种视频格式 42 | 43 | ::: tip 💡 使用建议 44 | - 通常建议选择所有常见格式以确保兼容性 45 | - 根据您的媒体库实际使用的格式进行调整 46 | - 可随时在设置中修改这些选项 47 | ::: 48 | 49 | --- 50 | 51 | ## 🎬 TMDB API 配置 52 | 53 | 自动获取电影和电视剧的详细信息(如演员、简介、评分等)。 54 | 55 | ### API 密钥配置 56 | - **TMDB API Key**:TMDB 提供的访问密钥 57 | - 支持**显示/隐藏**功能保护密钥安全 58 | - 请在 [TMDB 官网](https://www.themoviedb.org/settings/api) 申请 API Key 59 | - 这是启用刮削功能的必需配置 60 | 61 | ### 语言和地区设置 62 | - **语言设置**: 63 | - 中文(简体)- zh-CN 64 | - 中文(繁体)- zh-TW 65 | - English - en-US 66 | - **地区设置**: 67 | - 中国 - CN 68 | - 台湾 - TW 69 | - 香港 - HK 70 | - 美国 - US 71 | 72 | ### HTTP 代理配置(可选) 73 | 如果需要通过代理访问 TMDB API: 74 | - **代理主机地址**:例如 `127.0.0.1` 75 | - **代理端口**:例如 `7890` 76 | 77 | ::: tip 💡 配置建议 78 | - TMDB API 密钥是免费的,注册后即可获得 79 | - 建议配置与您主要使用语言相匹配的语言设置 80 | - 如果网络环境无法直接访问 TMDB,可以配置代理 81 | ::: 82 | 83 | --- 84 | 85 | ## ⚙️ 刮削设置 86 | 87 | 配置媒体信息的获取和处理方式。 88 | 89 | ### 刮削功能开关 90 | - **启用刮削功能**:总开关,控制是否启用媒体信息自动获取 91 | 92 | ### 刮削选项 93 | - **生成 NFO 文件**:为媒体创建 Kodi/Plex 兼容的信息文件 94 | - **下载海报图片**:自动获取电影/电视剧海报图片 95 | 96 | ::: tip 💡 使用建议 97 | - 首先需要配置 TMDB API 密钥才能启用刮削功能 98 | - 建议开启 NFO 文件生成,便于媒体库管理 99 | - 海报图片可以丰富媒体展示效果,建议开启 100 | - 刮削功能会在任务执行时自动运行 101 | ::: 102 | 103 | --- 104 | 105 | ## 🔧 其他设置 106 | 107 | ### 保存配置 108 | 所有配置修改完成后,记得点击页面底部的 **"保存"** 按钮来应用更改。 109 | 110 | ::: tip 💡 实用建议 111 | - 修改配置后记得点击保存按钮 112 | - TMDB API 密钥是免费的,注册即可获得 113 | - 建议根据实际需要选择媒体格式,不一定要全选 114 | ::: 115 | 116 | --- 117 | 118 | ## 📋 可用功能 119 | 120 | 当前版本提供以下配置功能: 121 | 122 | - **📁 媒体文件设置**:选择要生成 STRM 文件的视频格式 123 | - **🎬 TMDB API 配置**:获取电影和电视剧的详细信息 124 | - **⚙️ 刮削设置**:配置媒体信息获取、NFO 文件生成和图片下载 125 | 126 | --- 127 | 128 | ## 💡 使用建议 129 | 130 | ### 新用户配置指南 131 | **必需设置**: 132 | 1. 选择您需要的 **媒体文件格式** 133 | 2. 配置 **TMDB API 密钥**(从 [TMDB 官网](https://www.themoviedb.org/settings/api) 获取) 134 | 135 | **可选设置**: 136 | 1. 配置 **语言和地区**偏好 137 | 2. 启用 **刮削功能**(自动获取媒体信息) 138 | 3. 配置 **代理设置**(如果网络需要) 139 | 140 | ### 使用建议 141 | - **首次使用**:建议开启刮削功能以获得完整的媒体信息 142 | - **网络较慢**:可以考虑配置代理来访问 TMDB 143 | - **存储空间有限**:选择主要的视频格式即可 144 | - **中文用户**:建议选择中文语言设置 145 | 146 | ### 故障排除 147 | **TMDB API 无法使用**: 148 | 1. 检查 API 密钥是否正确 149 | 2. 确认网络连接正常 150 | 3. 考虑使用代理(如果网络受限) 151 | 152 | **刮削功能不工作**: 153 | 1. 确认已启用刮削功能开关 154 | 2. 检查 TMDB API 密钥是否已配置 155 | 3. 验证网络连接是否正常 156 | 157 | **刮削效果不佳**: 158 | 1. 检查媒体文件命名是否规范 159 | 2. 确认 TMDB API 连接正常 160 | 3. 尝试使用 AI 识别辅助 161 | 162 | --- 163 | 164 | ## 🆘 获取帮助 165 | 166 | 如果在设置过程中遇到问题: 167 | 168 | 1. **查看提示信息**:注意页面上的提示和说明 169 | 2. **参考相关文档**: 170 | - [常见问题](./faq.md) 171 | - [快速开始](./quick-start.md) 172 | - [开发指南](./dev.md) 173 | 3. **联系支持**: 174 | - 在 GitHub 提交 Issue 175 | - 查看项目文档获取更多信息 176 | 177 | --- 178 | 179 | 合理的系统设置可以让 OpenList to Stream 更好地为您服务。建议根据自己的使用习惯和网络环境,逐步调整各项设置,找到最适合您的配置方案。 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/media/MediaInfo.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.media; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * 媒体信息DTO 8 | * 9 | * @author hienao 10 | * @since 2024-01-01 11 | */ 12 | @Data 13 | @Accessors(chain = true) 14 | public class MediaInfo { 15 | 16 | /** 媒体类型 */ 17 | private MediaType type; 18 | 19 | /** 标题 */ 20 | private String title; 21 | 22 | /** 年份 */ 23 | private String year; 24 | 25 | /** 季数(电视剧) */ 26 | private Integer season; 27 | 28 | /** 集数(电视剧) */ 29 | private Integer episode; 30 | 31 | /** 原始文件名 */ 32 | private String originalFileName; 33 | 34 | /** 清理后的标题 */ 35 | private String cleanTitle; 36 | 37 | /** 是否包含年份信息 */ 38 | private boolean hasYear; 39 | 40 | /** 是否包含季集信息 */ 41 | private boolean hasSeasonEpisode; 42 | 43 | /** 解析置信度(0-100) */ 44 | private int confidence; 45 | 46 | /** 媒体类型枚举 */ 47 | public enum MediaType { 48 | /** 电影 */ 49 | MOVIE, 50 | /** 电视剧 */ 51 | TV_SHOW, 52 | /** 未知 */ 53 | UNKNOWN 54 | } 55 | 56 | /** 57 | * 获取显示标题 58 | * 59 | * @return 显示标题 60 | */ 61 | public String getDisplayTitle() { 62 | if (cleanTitle != null && !cleanTitle.isEmpty()) { 63 | return cleanTitle; 64 | } 65 | return title; 66 | } 67 | 68 | /** 69 | * 获取搜索关键词 70 | * 71 | * @return 搜索关键词 72 | */ 73 | public String getSearchQuery() { 74 | String query = getDisplayTitle(); 75 | if (query == null || query.isEmpty()) { 76 | return originalFileName; 77 | } 78 | 79 | // 清理搜索查询,移除可能的季集信息(作为保险措施) 80 | // 移除常见的季集格式:S01E01, S1E1, Season 1 Episode 1等 81 | query = 82 | query 83 | .replaceAll("(?i)_?S\\d{1,2}E\\d{1,2}.*$", "") // S01E01格式 84 | .replaceAll("(?i)_?Season\\s*\\d+\\s*Episode\\s*\\d+.*$", "") // Season X Episode Y格式 85 | .replaceAll("(?i)_?第\\d+季第\\d+集.*$", "") // 中文季集格式 86 | .replaceAll("_+$", "") // 移除末尾的下划线 87 | .trim(); 88 | 89 | return query.isEmpty() ? originalFileName : query; 90 | } 91 | 92 | /** 93 | * 是否为电影 94 | * 95 | * @return 是否为电影 96 | */ 97 | public boolean isMovie() { 98 | return MediaType.MOVIE.equals(type); 99 | } 100 | 101 | /** 102 | * 是否为电视剧 103 | * 104 | * @return 是否为电视剧 105 | */ 106 | public boolean isTvShow() { 107 | return MediaType.TV_SHOW.equals(type); 108 | } 109 | 110 | /** 111 | * 获取季集字符串 112 | * 113 | * @return 季集字符串,如 "S01E01" 114 | */ 115 | public String getSeasonEpisodeString() { 116 | if (season != null && episode != null) { 117 | return String.format("S%02dE%02d", season, episode); 118 | } else if (season != null) { 119 | return String.format("S%02d", season); 120 | } 121 | return null; 122 | } 123 | 124 | /** 125 | * 构建完整标题(包含年份和季集信息) 126 | * 127 | * @return 完整标题 128 | */ 129 | public String getFullTitle() { 130 | StringBuilder sb = new StringBuilder(); 131 | sb.append(getDisplayTitle()); 132 | 133 | if (hasYear && year != null) { 134 | sb.append(" (").append(year).append(")"); 135 | } 136 | 137 | if (hasSeasonEpisode && getSeasonEpisodeString() != null) { 138 | sb.append(" ").append(getSeasonEpisodeString()); 139 | } 140 | 141 | return sb.toString(); 142 | } 143 | 144 | @Override 145 | public String toString() { 146 | return String.format( 147 | "MediaInfo{type=%s, title='%s', year='%s', season=%d, episode=%d, confidence=%d}", 148 | type, title, year, season, episode, confidence); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/tmdb/TmdbSearchResponse.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto.tmdb; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | import lombok.Data; 6 | 7 | /** 8 | * TMDB 搜索响应DTO 9 | * 10 | * @author hienao 11 | * @since 2024-01-01 12 | */ 13 | @Data 14 | public class TmdbSearchResponse { 15 | 16 | /** 当前页码 */ 17 | private Integer page; 18 | 19 | /** 搜索结果列表 */ 20 | private List results; 21 | 22 | /** 总结果数 */ 23 | @JsonProperty("total_results") 24 | private Integer totalResults; 25 | 26 | /** 总页数 */ 27 | @JsonProperty("total_pages") 28 | private Integer totalPages; 29 | 30 | /** TMDB 搜索结果项 */ 31 | @Data 32 | public static class TmdbSearchResult { 33 | 34 | /** TMDB ID */ 35 | private Integer id; 36 | 37 | /** 标题(电影) */ 38 | private String title; 39 | 40 | /** 名称(电视剧) */ 41 | private String name; 42 | 43 | /** 原始标题 */ 44 | @JsonProperty("original_title") 45 | private String originalTitle; 46 | 47 | /** 原始名称 */ 48 | @JsonProperty("original_name") 49 | private String originalName; 50 | 51 | /** 概述 */ 52 | private String overview; 53 | 54 | /** 海报路径 */ 55 | @JsonProperty("poster_path") 56 | private String posterPath; 57 | 58 | /** 背景图路径 */ 59 | @JsonProperty("backdrop_path") 60 | private String backdropPath; 61 | 62 | /** 发布日期(电影) */ 63 | @JsonProperty("release_date") 64 | private String releaseDate; 65 | 66 | /** 首播日期(电视剧) */ 67 | @JsonProperty("first_air_date") 68 | private String firstAirDate; 69 | 70 | /** 媒体类型:movie 或 tv */ 71 | @JsonProperty("media_type") 72 | private String mediaType; 73 | 74 | /** 成人内容标识 */ 75 | private Boolean adult; 76 | 77 | /** 语言 */ 78 | @JsonProperty("original_language") 79 | private String originalLanguage; 80 | 81 | /** 流行度 */ 82 | private Double popularity; 83 | 84 | /** 评分 */ 85 | @JsonProperty("vote_average") 86 | private Double voteAverage; 87 | 88 | /** 评分人数 */ 89 | @JsonProperty("vote_count") 90 | private Integer voteCount; 91 | 92 | /** 类型ID列表 */ 93 | @JsonProperty("genre_ids") 94 | private List genreIds; 95 | 96 | /** 97 | * 获取显示标题 98 | * 99 | * @return 显示标题 100 | */ 101 | public String getDisplayTitle() { 102 | if (title != null && !title.isEmpty()) { 103 | return title; 104 | } 105 | if (name != null && !name.isEmpty()) { 106 | return name; 107 | } 108 | if (originalTitle != null && !originalTitle.isEmpty()) { 109 | return originalTitle; 110 | } 111 | if (originalName != null && !originalName.isEmpty()) { 112 | return originalName; 113 | } 114 | return "未知标题"; 115 | } 116 | 117 | /** 118 | * 获取发布年份 119 | * 120 | * @return 发布年份 121 | */ 122 | public String getReleaseYear() { 123 | String date = releaseDate != null ? releaseDate : firstAirDate; 124 | if (date != null && date.length() >= 4) { 125 | return date.substring(0, 4); 126 | } 127 | return null; 128 | } 129 | 130 | /** 131 | * 判断是否为电影 132 | * 133 | * @return 是否为电影 134 | */ 135 | public boolean isMovie() { 136 | return "movie".equals(mediaType) || (title != null && !title.isEmpty()); 137 | } 138 | 139 | /** 140 | * 判断是否为电视剧 141 | * 142 | * @return 是否为电视剧 143 | */ 144 | public boolean isTvShow() { 145 | return "tv".equals(mediaType) || (name != null && !name.isEmpty()); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /frontend/utils/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API 工具函数 3 | * 解决 Docker 端口映射时的 API 调用问题 4 | */ 5 | 6 | /** 7 | * 获取 API 基础 URL 8 | * 统一开发和生产环境的API调用方式 9 | */ 10 | export function getApiBaseUrl() { 11 | const config = useRuntimeConfig() 12 | 13 | // 直接使用配置的 API 基础路径 14 | // 开发环境: http://localhost:8080/api 15 | // 生产环境: /api (相对路径,由 Nginx 代理) 16 | return config.public.apiBase 17 | } 18 | 19 | /** 20 | * 统一的 API 调用函数 21 | * @param {string} endpoint - API 端点路径(如 '/auth/sign-in') 22 | * @param {object} options - fetch 选项 23 | * @returns {Promise} - API 响应 24 | */ 25 | export async function apiCall(endpoint, options = {}) { 26 | const baseUrl = getApiBaseUrl() 27 | // 确保 endpoint 以 / 开头,避免重复的 /api 前缀 28 | const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}` 29 | const url = `${baseUrl}${cleanEndpoint}` 30 | 31 | // 默认选项 32 | const defaultOptions = { 33 | method: 'GET', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | ...options.headers 37 | } 38 | } 39 | 40 | // 合并选项 41 | const finalOptions = { 42 | ...defaultOptions, 43 | ...options, 44 | headers: { 45 | ...defaultOptions.headers, 46 | ...options.headers 47 | } 48 | } 49 | 50 | try { 51 | return await $fetch(url, finalOptions) 52 | } catch (error) { 53 | console.error(`API 调用失败: ${url}`, error) 54 | 55 | // 全局处理 401 未授权错误 56 | if (error.status === 401) { 57 | await handleUnauthorizedError() 58 | } 59 | 60 | // 处理响应体中的错误信息 61 | if (error.data) { 62 | // 如果错误响应中包含 ApiResponse 格式的数据 63 | const errorData = error.data 64 | if (errorData.message) { 65 | // 创建一个新的错误对象,包含正确的错误信息 66 | const enhancedError = new Error(errorData.message) 67 | enhancedError.status = error.status 68 | enhancedError.data = error.data 69 | enhancedError.code = errorData.code 70 | throw enhancedError 71 | } 72 | } 73 | 74 | throw error 75 | } 76 | } 77 | 78 | /** 79 | * 处理 401 未授权错误的统一逻辑 80 | */ 81 | async function handleUnauthorizedError() { 82 | console.warn('检测到 401 未授权错误,清除 token 并跳转到登录页') 83 | 84 | // 使用认证store清除认证信息 85 | const { useAuthStore } = await import('~/stores/auth.js') 86 | const authStore = useAuthStore() 87 | authStore.clearAuth() 88 | 89 | // 只在客户端执行跳转,避免服务端渲染时的问题 90 | if (import.meta.client) { 91 | // 检查用户是否存在,决定跳转到登录页还是注册页 92 | try { 93 | const response = await $fetch(`${getApiBaseUrl()}/auth/check-user`, { 94 | method: 'GET' 95 | }) 96 | 97 | if (response.code === 200 && response.data?.exists) { 98 | // 用户存在,跳转到登录页 99 | await navigateTo('/login') 100 | } else { 101 | // 用户不存在,跳转到注册页 102 | await navigateTo('/register') 103 | } 104 | } catch (checkError) { 105 | console.error('检查用户失败,默认跳转到登录页:', checkError) 106 | // 检查失败时默认跳转到登录页 107 | await navigateTo('/login') 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * 带认证的 API 调用函数 114 | * @param {string} endpoint - API 端点路径 115 | * @param {object} options - fetch 选项 116 | * @returns {Promise} - API 响应 117 | */ 118 | export async function authenticatedApiCall(endpoint, options = {}) { 119 | // 使用认证store获取token 120 | const { useAuthStore } = await import('~/stores/auth.js') 121 | const authStore = useAuthStore() 122 | const token = authStore.getToken 123 | 124 | if (!token) { 125 | throw new Error('未找到认证令牌') 126 | } 127 | 128 | const authOptions = { 129 | ...options, 130 | headers: { 131 | 'Authorization': `Bearer ${token}`, 132 | ...options.headers 133 | } 134 | } 135 | 136 | return apiCall(endpoint, authOptions) 137 | } -------------------------------------------------------------------------------- /frontend/middleware/auth.js: -------------------------------------------------------------------------------- 1 | // 认证中间件 - 保护需要登录的页面 2 | import { apiCall } from '~/utils/api.js' 3 | import { shouldRefreshToken, validateTokenWithBackend } from '~/utils/token.js' 4 | import logger from '~/utils/logger.js' 5 | 6 | export default defineNuxtRouteMiddleware(async (to, from) => { 7 | logger.info('Auth中间件执行:', { to: to.path, from: from?.path }) 8 | 9 | // 获取认证store 10 | const { useAuthStore } = await import('~/stores/auth.js') 11 | const authStore = useAuthStore() 12 | 13 | // 尝试恢复认证状态 14 | authStore.restoreAuth() 15 | 16 | logger.info('Auth中间件 - 认证状态:', { 17 | token: authStore.getToken, 18 | isAuthenticated: authStore.isAuthenticated 19 | }) 20 | 21 | // 如果当前页面是登录或注册页面,只检查用户是否存在,不进行token验证 22 | if (to.path === '/login' || to.path === '/register') { 23 | logger.info('Auth中间件 - 访问登录/注册页面,执行用户检查') 24 | return await handleAuthPages(to) 25 | } 26 | 27 | // 如果没有认证,检查用户是否存在后跳转 28 | if (!authStore.isAuthenticated) { 29 | logger.info('Auth中间件 - 未认证,准备跳转') 30 | const redirectPath = await checkUserAndRedirect() 31 | return navigateTo(redirectPath) 32 | } 33 | 34 | logger.info('Auth中间件 - 认证验证通过,允许访问页面') 35 | 36 | // 对于重要页面,进行后端验证(可选,避免每次都验证影响性能) 37 | // 这里可以根据需要启用后端验证 38 | const shouldValidateWithBackend = false // 可以根据页面重要性决定 39 | if (shouldValidateWithBackend) { 40 | const isBackendValid = await validateTokenWithBackend(token.value) 41 | if (!isBackendValid) { 42 | logger.warn('Token后端验证失败,清除token') 43 | clearAuthCookies() 44 | const redirectPath = await checkUserAndRedirect() 45 | return navigateTo(redirectPath) 46 | } 47 | } 48 | 49 | // 检查token是否需要刷新(剩余有效期在7-14天之间) 50 | if (shouldRefreshToken(authStore.getToken)) { 51 | // 在后台刷新token,不阻塞页面加载 52 | refreshTokenInBackground(authStore) 53 | } 54 | }) 55 | 56 | 57 | 58 | // 处理登录和注册页面的逻辑 59 | async function handleAuthPages(to) { 60 | try { 61 | const response = await apiCall('/auth/check-user', { 62 | method: 'GET' 63 | }) 64 | 65 | if (response.code === 200 && response.data?.exists) { 66 | // 用户存在 67 | if (to.path === '/register') { 68 | // 如果访问注册页但用户已存在,跳转到登录页 69 | return navigateTo('/login') 70 | } 71 | // 如果访问登录页且用户存在,允许访问 72 | return 73 | } else { 74 | // 用户不存在 75 | if (to.path === '/login') { 76 | // 如果访问登录页但用户不存在,跳转到注册页 77 | return navigateTo('/register') 78 | } 79 | // 如果访问注册页且用户不存在,允许访问 80 | return 81 | } 82 | } catch (error) { 83 | logger.error('检查用户失败:', error) 84 | // 检查失败时允许访问当前页面 85 | return 86 | } 87 | } 88 | 89 | // 检查用户是否存在并决定跳转路径 90 | async function checkUserAndRedirect() { 91 | try { 92 | const response = await apiCall('/auth/check-user', { 93 | method: 'GET' 94 | }) 95 | 96 | if (response.code === 200 && response.data?.exists) { 97 | // 用户存在,跳转到登录页 98 | return '/login' 99 | } else { 100 | // 用户不存在,跳转到注册页 101 | return '/register' 102 | } 103 | } catch (error) { 104 | logger.error('检查用户失败:', error) 105 | // 检查失败时默认跳转到登录页 106 | return '/login' 107 | } 108 | } 109 | 110 | // 后台刷新token 111 | async function refreshTokenInBackground(authStore) { 112 | try { 113 | const { authenticatedApiCall } = await import('~/utils/api.js') 114 | const response = await authenticatedApiCall('/auth/refresh', { 115 | method: 'POST' 116 | }) 117 | 118 | if (response.code === 200 && response.data?.token) { 119 | // 更新token 120 | authStore.updateToken(response.data.token) 121 | logger.info('Token已自动刷新') 122 | } 123 | } catch (error) { 124 | logger.error('Token刷新失败:', error) 125 | // 刷新失败不影响当前页面使用,token仍然有效 126 | } 127 | } -------------------------------------------------------------------------------- /docs/add-openlist.md: -------------------------------------------------------------------------------- 1 | # 添加 OpenList 配置 2 | 3 | OpenList 配置是连接您的 OpenList 服务器和 STRM 转换任务的桥梁。本页详细介绍如何添加和管理 OpenList 配置。 4 | 5 | ## 什么是 OpenList 配置? 6 | 7 | OpenList 配置包含了连接到您的 OpenList 服务器所需的所有信息: 8 | 9 | - 服务器地址(Base URL) 10 | - 认证令牌(Token) 11 | - STRM Base URL(可选) 12 | - URL 编码设置 13 | 14 | ## 添加新配置 15 | 16 | ### 第一步:访问配置页面 17 | 18 | 1. 登录 OpenList to Stream 系统 19 | 2. 在首页点击 **"添加配置"** 按钮 20 | 21 | ### 第二步:填写配置信息 22 | 23 | #### Base URL 24 | 填写您的 OpenList 服务器地址: 25 | 26 | **格式示例:** 27 | - `http://192.168.1.100:5244`(局域网地址) 28 | - `https://openlist.example.com`(域名) 29 | - `http://localhost:5244`(本地部署) 30 | 31 | ::: tip 注意事项 32 | - 地址必须包含协议(http:// 或 https://) 33 | - 确保端口号正确(默认:5244) 34 | - 如果使用域名,确保域名可以正常解析 35 | ::: 36 | 37 | #### Token 38 | 填写 OpenList 的访问令牌: 39 | 40 | **获取方式:** 41 | 1. 登录您的 OpenList 服务器 42 | 2. 进入设置页面或 API 配置页面 43 | 3. 复制生成的 Token 44 | 45 | ::: warning 安全提醒 46 | - Token 相当于密码,请妥善保管 47 | - 建议定期更换 Token 48 | - 不要在不安全的环境中分享 Token 49 | ::: 50 | 51 | #### STRM Base URL(可选) 52 | 用于替换 STRM 文件中的原始 URL: 53 | 54 | **使用场景:** 55 | - 媒体服务器与 OpenList 服务器部署在不同地址 56 | - 需要通过内网地址访问媒体文件 57 | - 使用 CDN 加速访问 58 | 59 | **示例:** 60 | - 留空:使用原始 OpenList 地址 61 | - `https://media-server.local`:使用内网媒体服务器地址 62 | - `https://cdn.example.com`:使用 CDN 地址 63 | 64 | ### 第三步:测试连接 65 | 66 | 填写完配置后,点击 **"测试连接"** 按钮: 67 | 68 | **✅ 连接成功** 69 | - 显示绿色成功提示 70 | - 可以看到服务器版本信息 71 | - 可以继续保存配置 72 | 73 | **❌ 连接失败** 74 | - 显示红色错误提示 75 | - 检查以下可能的问题: 76 | - 服务器地址是否正确 77 | - 网络连接是否正常 78 | - Token 是否正确且有效 79 | - 服务器是否正在运行 80 | - Token 是否有足够的权限访问指定路径 81 | 82 | ### 第四步:保存配置 83 | 84 | 连接测试成功后,点击 **"保存"** 按钮完成配置创建。 85 | 86 | ## 管理现有配置 87 | 88 | ### 查看配置列表 89 | 在配置页面可以看到所有已添加的配置: 90 | 91 | | 配置名称 | 服务器地址 | 状态 | 操作 | 92 | |----------|------------|------|------| 93 | | 家庭媒体服务器 | http://192.168.1.100:5244 | ✅ 正常 | 编辑/删除/测试 | 94 | | 电影库 | https://movies.example.com | ❌ 失败 | 编辑/删除/测试 | 95 | 96 | ### 编辑配置 97 | 1. 在配置列表中找到要编辑的配置 98 | 2. 点击右侧的 **"编辑"** 按钮 99 | 3. 修改配置信息 100 | 4. 点击 **"测试连接"** 验证 101 | 5. 点击 **"保存"** 确认修改 102 | 103 | ### 删除配置 104 | 1. 在配置列表中找到要删除的配置 105 | 2. 点击右侧的 **"删除"** 按钮 106 | 3. 在确认对话框中点击 **"确认删除"** 107 | 108 | ::: danger 删除警告 109 | 删除配置后,所有使用该配置的任务将无法正常工作。请确保没有任务正在使用该配置。 110 | ::: 111 | 112 | ### 测试配置 113 | 1. 在配置列表中找到要测试的配置 114 | 2. 点击右侧的 **"测试"** 按钮 115 | 3. 查看测试结果 116 | 117 | ## 常见连接问题 118 | 119 | ### 问题 1:连接超时 120 | **可能原因:** 121 | - 网络连接问题 122 | - 服务器地址错误 123 | - 防火墙阻止连接 124 | 125 | **解决方法:** 126 | 1. 检查网络连接 127 | 2. 验证服务器地址和端口 128 | 3. 检查防火墙设置 129 | 130 | ### 问题 2:认证失败 131 | **可能原因:** 132 | - Token 错误或已过期 133 | - Token 权限不足 134 | - Token 格式不正确 135 | 136 | **解决方法:** 137 | 1. 重新生成或复制 Token 138 | 2. 检查 Token 是否有足够的访问权限 139 | 3. 确认 Token 没有过期 140 | 4. 验证 Token 格式是否正确 141 | 142 | ### 问题 3:SSL 证书错误 143 | **可能原因:** 144 | - 使用自签名证书 145 | - 证书过期 146 | 147 | **解决方法:** 148 | 1. 禁用 SSL 验证(仅测试环境) 149 | 2. 更新证书 150 | 3. 使用有效的 SSL 证书 151 | 152 | ## 最佳实践 153 | 154 | ### 1. 命名规范 155 | 使用描述性的配置名称: 156 | - ✅ `家庭电影库-阿里云盘` 157 | - ✅ `电视剧库-NAS` 158 | - ❌ `配置1` 159 | - ❌ `test` 160 | 161 | ### 2. 网络优化 162 | - 优先使用局域网地址(更快) 163 | - 确保网络连接稳定 164 | - 考虑使用 CDN 加速 165 | 166 | ### 3. 安全考虑 167 | - 定期更新 Token 168 | - 确保 Token 安全,避免泄露 169 | - 启用 HTTPS(如可能) 170 | - 为 Token 设置合适的权限范围 171 | 172 | ### 4. 备份配置 173 | 定期备份重要的配置信息,以防意外丢失。 174 | 175 | ## 在任务中使用配置 176 | 177 | 配置创建后,可以通过以下方式使用: 178 | 179 | 1. 在首页的配置列表中,找到对应的配置 180 | 2. 点击操作列中的 **"管理任务"** 按钮 181 | 3. 在任务管理页面中创建和管理转换任务 182 | 4. 任务会自动使用关联的 OpenList 配置 183 | 184 | ## 故障排除 185 | 186 | ### 查看连接日志 187 | 在系统日志中可以查看详细的连接信息: 188 | 1. 访问 [日志页面](./log.md) 189 | 2. 筛选 "OpenList 连接" 相关日志 190 | 3. 查看具体的错误信息 191 | 192 | ### 联系支持 193 | 如果问题仍未解决,可以: 194 | 1. 查看 [常见问题](./faq.md) 195 | 2. 在 [GitHub Issues](https://github.com/hienao/ostrm/issues) 提交问题 196 | 3. 提供详细的错误信息和配置内容 197 | 198 | --- 199 | 200 | 现在您已经学会了如何添加和管理 OpenList 配置,接下来可以 [创建您的第一个转换任务](./add-task.md)。 -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/PathConfigurationValidator.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.ApplicationContextAware; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 路径配置验证器 13 | * 14 | *

验证应用程序中所有路径配置的有效性和可访问性 15 | * 16 | * @author hienao 17 | * @since 2024-01-01 18 | */ 19 | @Slf4j 20 | @Component 21 | public class PathConfigurationValidator implements ApplicationContextAware { 22 | 23 | @Autowired private ApplicationContext applicationContext; 24 | 25 | @Override 26 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 27 | this.applicationContext = applicationContext; 28 | } 29 | 30 | @PostConstruct 31 | public void validatePaths() { 32 | try { 33 | PathConfiguration config = applicationContext.getBean(PathConfiguration.class); 34 | 35 | log.info("=== 开始验证路径配置 ==="); 36 | 37 | validateDirectoryPath(config.getLogs(), "logs"); 38 | validateDirectoryPath(config.getData(), "data"); 39 | validateDirectoryPath(config.getConfig(), "config"); 40 | validateDirectoryPath(config.getStrm(), "strm"); 41 | 42 | // 验证数据库文件路径(不需要是目录) 43 | validateFilePath(config.getDatabase(), "database"); 44 | 45 | // 验证用户信息文件路径(不需要是目录) 46 | validateFilePath(config.getUserInfo(), "userInfo"); 47 | 48 | // 验证前端日志路径 49 | validateDirectoryPath(config.getFrontendLogs(), "frontendLogs"); 50 | 51 | log.info("=== 路径配置验证完成 ==="); 52 | } catch (Exception e) { 53 | log.error("路径配置验证失败: {}", e.getMessage(), e); 54 | } 55 | } 56 | 57 | /** 58 | * 验证目录路径 59 | * 60 | * @param path 路径 61 | * @param pathName 路径名称 62 | */ 63 | private void validateDirectoryPath(String path, String pathName) { 64 | java.io.File directory = new java.io.File(path); 65 | 66 | if (!directory.exists()) { 67 | log.warn("{} 目录不存在: {}", pathName, path); 68 | // 尝试创建目录 69 | try { 70 | if (directory.mkdirs()) { 71 | log.info("成功创建 {} 目录: {}", pathName, path); 72 | } else { 73 | log.error("无法创建 {} 目录: {}", pathName, path); 74 | } 75 | } catch (Exception e) { 76 | log.error("创建 {} 目录时发生错误: {} - {}", pathName, path, e.getMessage(), e); 77 | } 78 | } else if (!directory.isDirectory()) { 79 | log.error("{} 路径不是目录: {}", pathName, path); 80 | } else if (!directory.canWrite()) { 81 | log.warn("{} 目录不可写: {}", pathName, path); 82 | } else { 83 | log.info("{} 目录验证成功: {}", pathName, path); 84 | } 85 | } 86 | 87 | /** 88 | * 验证文件路径 89 | * 90 | * @param path 路径 91 | * @param pathName 路径名称 92 | */ 93 | private void validateFilePath(String path, String pathName) { 94 | java.io.File file = new java.io.File(path); 95 | 96 | if (!file.exists()) { 97 | // 如果是数据库文件,检查父目录是否存在并可写 98 | java.io.File parentDir = file.getParentFile(); 99 | if (parentDir != null && !parentDir.exists()) { 100 | log.warn("{} 文件不存在,检查父目录: {}", pathName, path); 101 | validateDirectoryPath(parentDir.getAbsolutePath(), pathName + "-parent"); 102 | } else { 103 | log.info("{} 文件不存在(正常): {}", pathName, path); 104 | } 105 | } else if (!file.isFile()) { 106 | log.error("{} 路径不是文件: {}", pathName, path); 107 | } else if (!file.canWrite()) { 108 | log.warn("{} 文件不可写: {}", pathName, path); 109 | } else { 110 | log.info("{} 文件验证成功: {}", pathName, path); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/controller/DataReportController.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.controller; 2 | 3 | import com.hienao.openlist2strm.service.DataReportService; 4 | import java.util.Map; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | /** 15 | * 数据上报控制器 16 | * 17 | * @author hienao 18 | * @since 2024-01-01 19 | */ 20 | @Slf4j 21 | @RestController 22 | @RequestMapping("/api/data-report") 23 | @RequiredArgsConstructor 24 | public class DataReportController { 25 | 26 | private final DataReportService dataReportService; 27 | 28 | /** 29 | * 上报事件数据 30 | * 31 | * @param event 事件名称 32 | * @param properties 自定义属性(可选) 33 | * @return 响应结果 34 | */ 35 | @PostMapping("/event") 36 | public ResponseEntity> reportEvent( 37 | @RequestParam String event, @RequestBody(required = false) Map properties) { 38 | try { 39 | dataReportService.reportEvent(event, properties); 40 | return ResponseEntity.ok(Map.of("success", true, "message", "事件数据上报成功")); 41 | } catch (Exception e) { 42 | log.error("事件数据上报失败: {}, 错误: {}", event, e.getMessage(), e); 43 | return ResponseEntity.ok(Map.of("success", false, "message", "事件数据上报失败: " + e.getMessage())); 44 | } 45 | } 46 | 47 | /** 48 | * 批量上报事件数据 49 | * 50 | * @param request 批量上报请求 51 | * @return 响应结果 52 | */ 53 | @PostMapping("/events") 54 | public ResponseEntity> reportEvents(@RequestBody BatchReportRequest request) { 55 | try { 56 | int successCount = 0; 57 | int failCount = 0; 58 | 59 | for (EventData eventData : request.getEvents()) { 60 | try { 61 | dataReportService.reportEvent(eventData.getEvent(), eventData.getProperties()); 62 | successCount++; 63 | } catch (Exception e) { 64 | log.warn("批量上报中单个事件失败: {}, 错误: {}", eventData.getEvent(), e.getMessage()); 65 | failCount++; 66 | } 67 | } 68 | 69 | return ResponseEntity.ok( 70 | Map.of( 71 | "success", 72 | true, 73 | "message", 74 | String.format("批量上报完成,成功: %d, 失败: %d", successCount, failCount), 75 | "successCount", 76 | successCount, 77 | "failCount", 78 | failCount)); 79 | } catch (Exception e) { 80 | log.error("批量事件数据上报失败, 错误: {}", e.getMessage(), e); 81 | return ResponseEntity.ok( 82 | Map.of("success", false, "message", "批量事件数据上报失败: " + e.getMessage())); 83 | } 84 | } 85 | 86 | /** 批量上报请求DTO */ 87 | public static class BatchReportRequest { 88 | private java.util.List events; 89 | 90 | public java.util.List getEvents() { 91 | return events; 92 | } 93 | 94 | public void setEvents(java.util.List events) { 95 | this.events = events; 96 | } 97 | } 98 | 99 | /** 事件数据DTO */ 100 | public static class EventData { 101 | private String event; 102 | private Map properties; 103 | 104 | public String getEvent() { 105 | return event; 106 | } 107 | 108 | public void setEvent(String event) { 109 | this.event = event; 110 | } 111 | 112 | public Map getProperties() { 113 | return properties; 114 | } 115 | 116 | public void setProperties(Map properties) { 117 | this.properties = properties; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/dto/PageRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.dto; 2 | 3 | import java.util.*; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | import lombok.*; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | public class PageRequestDto { 12 | 13 | public static final String REGEX = "^[a-zA-Z][a-zA-Z0-9_]*$"; 14 | 15 | public static final String SPACE = " "; 16 | 17 | private int page; 18 | private int size; 19 | 20 | private Map sortBy = new HashMap<>(); 21 | 22 | public PageRequestDto(int page, int size) { 23 | checkPageAndSize(page, size); 24 | this.page = page; 25 | this.size = size; 26 | } 27 | 28 | public PageRequestDto(int page, int size, Map sortBy) { 29 | checkPageAndSize(page, size); 30 | this.page = page; 31 | this.size = size; 32 | this.sortBy = sortBy; 33 | } 34 | 35 | @AllArgsConstructor 36 | @Getter 37 | public enum Direction { 38 | ASC("ASC"), 39 | DESC("DESC"); 40 | 41 | private final String keyword; 42 | 43 | public static Direction fromString(String value) { 44 | try { 45 | return Direction.valueOf(value.toUpperCase(Locale.US)); 46 | } catch (Exception e) { 47 | throw new IllegalArgumentException( 48 | String.format( 49 | "Invalid value '%s' for orders given; Has to be either 'desc' or 'asc' (case" 50 | + " insensitive)", 51 | value), 52 | e); 53 | } 54 | } 55 | } 56 | 57 | public static PageRequestDto of(int page, int size) { 58 | return new PageRequestDto(page, size); 59 | } 60 | 61 | public static PageRequestDto of(int page, int size, Map sortBy) { 62 | return new PageRequestDto(page, size, sortBy); 63 | } 64 | 65 | public String getSortSql() { 66 | if (sortBy.isEmpty()) { 67 | return ""; 68 | } 69 | List orderClauses = new ArrayList<>(); 70 | for (Map.Entry entry : sortBy.entrySet()) { 71 | orderClauses.add(entry.getKey() + " " + entry.getValue().getKeyword()); 72 | } 73 | return "ORDER BY " + String.join(", ", orderClauses); 74 | } 75 | 76 | private void checkPageAndSize(int page, int size) { 77 | if (page < 0) { 78 | throw new IllegalArgumentException("Page index must not be less than zero"); 79 | } 80 | 81 | if (size < 1) { 82 | throw new IllegalArgumentException("Page size must not be less than one"); 83 | } 84 | } 85 | 86 | public long getOffset() { 87 | return (long) page * (long) size; 88 | } 89 | 90 | public void setSortBy(String sortBy) { 91 | this.sortBy = convertSortBy(sortBy); 92 | } 93 | 94 | private Map convertSortBy(String sortBy) { 95 | Map result = new HashMap<>(); 96 | if (StringUtils.isEmpty(sortBy)) { 97 | return result; 98 | } 99 | for (String fieldSpaceDirection : sortBy.split(",")) { 100 | String[] fieldDirectionArray = fieldSpaceDirection.split(SPACE); 101 | if (fieldDirectionArray.length != 2) { 102 | throw new IllegalArgumentException( 103 | String.format( 104 | "Invalid sortBy field format %s. The expect format is [col1 asc,col2 desc]", 105 | sortBy)); 106 | } 107 | String field = fieldDirectionArray[0]; 108 | if (!verifySortField(field)) { 109 | throw new IllegalArgumentException( 110 | String.format("Invalid Sort field %s. Sort field must match %s", sortBy, REGEX)); 111 | } 112 | String direction = fieldDirectionArray[1]; 113 | result.put(field, Direction.fromString(direction)); 114 | } 115 | return result; 116 | } 117 | 118 | private static boolean verifySortField(String sortField) { 119 | Pattern pattern = Pattern.compile(REGEX); 120 | Matcher matcher = pattern.matcher(sortField); 121 | return matcher.matches(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hienao/openlist2strm/config/security/Jwt.java: -------------------------------------------------------------------------------- 1 | package com.hienao.openlist2strm.config.security; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTVerificationException; 7 | import com.auth0.jwt.interfaces.DecodedJWT; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import java.time.LocalDateTime; 11 | import java.time.ZoneId; 12 | import java.util.Date; 13 | import lombok.Getter; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.stereotype.Component; 18 | 19 | @Slf4j 20 | @Component 21 | @Getter 22 | public class Jwt { 23 | 24 | private final String secret; 25 | 26 | private final int expirationMin; 27 | 28 | private final JWTVerifier verifier; 29 | 30 | public Jwt( 31 | @Value("${jwt.secret}") String secret, @Value("${jwt.expiration-min}") int expirationMin) { 32 | this.verifier = JWT.require(Algorithm.HMAC256(secret)).build(); 33 | this.secret = secret; 34 | this.expirationMin = expirationMin; 35 | } 36 | 37 | public String getSubject(String token) { 38 | return JWT.decode(token).getSubject(); 39 | } 40 | 41 | public Date getExpiresAt(String token) { 42 | return JWT.decode(token).getExpiresAt(); 43 | } 44 | 45 | public Date getIssuedAt(String token) { 46 | return JWT.decode(token).getIssuedAt(); 47 | } 48 | 49 | public boolean shouldRefresh(String token) { 50 | try { 51 | DecodedJWT decodedJWT = JWT.decode(token); 52 | Date issuedAt = decodedJWT.getIssuedAt(); 53 | Date expiresAt = decodedJWT.getExpiresAt(); 54 | Date now = new Date(); 55 | 56 | // 计算token已使用时间(分钟) 57 | long usedMinutes = (now.getTime() - issuedAt.getTime()) / (1000 * 60); 58 | // 计算token剩余时间(分钟) 59 | long remainingMinutes = (expiresAt.getTime() - now.getTime()) / (1000 * 60); 60 | 61 | // 如果已使用超过7天(10080分钟)且剩余时间少于7天,则需要刷新 62 | return usedMinutes > 10080 && remainingMinutes < 10080; 63 | } catch (Exception e) { 64 | log.warn("检查token刷新状态失败", e); 65 | return false; 66 | } 67 | } 68 | 69 | public Boolean verify(String token) { 70 | try { 71 | verifier.verify(token); 72 | return Boolean.TRUE; 73 | } catch (JWTVerificationException e) { 74 | return Boolean.FALSE; 75 | } 76 | } 77 | 78 | public String extract(HttpServletRequest request) { 79 | String authorization = request.getHeader("Authorization"); 80 | if (StringUtils.isNotEmpty(authorization) && authorization.startsWith("Bearer")) { 81 | return authorization.substring(7); 82 | } else { 83 | return null; 84 | } 85 | } 86 | 87 | public String create(String userIdentify) { 88 | return JWT.create() 89 | .withSubject(String.valueOf(userIdentify)) 90 | .withIssuedAt(new Date()) 91 | .withExpiresAt( 92 | Date.from( 93 | LocalDateTime.now() 94 | .plusMinutes(expirationMin) 95 | .atZone(ZoneId.systemDefault()) 96 | .toInstant())) 97 | .sign(Algorithm.HMAC256(secret)); 98 | } 99 | 100 | public String makeToken( 101 | HttpServletRequest request, HttpServletResponse response, String userIdentify) { 102 | String token = create(userIdentify); 103 | response.addHeader("Authorization", String.format("Bearer %s", token)); 104 | return token; 105 | } 106 | 107 | public String refreshToken(String oldToken) { 108 | try { 109 | String subject = getSubject(oldToken); 110 | return create(subject); 111 | } catch (Exception e) { 112 | log.error("刷新token失败", e); 113 | return null; 114 | } 115 | } 116 | 117 | public void removeToken(HttpServletRequest request, HttpServletResponse response) { 118 | response.addHeader("Authorization", null); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /.bmad-core/data/test-levels-framework.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Test Levels Framework 4 | 5 | Comprehensive guide for determining appropriate test levels (unit, integration, E2E) for different scenarios. 6 | 7 | ## Test Level Decision Matrix 8 | 9 | ### Unit Tests 10 | 11 | **When to use:** 12 | 13 | - Testing pure functions and business logic 14 | - Algorithm correctness 15 | - Input validation and data transformation 16 | - Error handling in isolated components 17 | - Complex calculations or state machines 18 | 19 | **Characteristics:** 20 | 21 | - Fast execution (immediate feedback) 22 | - No external dependencies (DB, API, file system) 23 | - Highly maintainable and stable 24 | - Easy to debug failures 25 | 26 | **Example scenarios:** 27 | 28 | ```yaml 29 | unit_test: 30 | component: 'PriceCalculator' 31 | scenario: 'Calculate discount with multiple rules' 32 | justification: 'Complex business logic with multiple branches' 33 | mock_requirements: 'None - pure function' 34 | ``` 35 | 36 | ### Integration Tests 37 | 38 | **When to use:** 39 | 40 | - Component interaction verification 41 | - Database operations and transactions 42 | - API endpoint contracts 43 | - Service-to-service communication 44 | - Middleware and interceptor behavior 45 | 46 | **Characteristics:** 47 | 48 | - Moderate execution time 49 | - Tests component boundaries 50 | - May use test databases or containers 51 | - Validates system integration points 52 | 53 | **Example scenarios:** 54 | 55 | ```yaml 56 | integration_test: 57 | components: ['UserService', 'AuthRepository'] 58 | scenario: 'Create user with role assignment' 59 | justification: 'Critical data flow between service and persistence' 60 | test_environment: 'In-memory database' 61 | ``` 62 | 63 | ### End-to-End Tests 64 | 65 | **When to use:** 66 | 67 | - Critical user journeys 68 | - Cross-system workflows 69 | - Visual regression testing 70 | - Compliance and regulatory requirements 71 | - Final validation before release 72 | 73 | **Characteristics:** 74 | 75 | - Slower execution 76 | - Tests complete workflows 77 | - Requires full environment setup 78 | - Most realistic but most brittle 79 | 80 | **Example scenarios:** 81 | 82 | ```yaml 83 | e2e_test: 84 | journey: 'Complete checkout process' 85 | scenario: 'User purchases with saved payment method' 86 | justification: 'Revenue-critical path requiring full validation' 87 | environment: 'Staging with test payment gateway' 88 | ``` 89 | 90 | ## Test Level Selection Rules 91 | 92 | ### Favor Unit Tests When: 93 | 94 | - Logic can be isolated 95 | - No side effects involved 96 | - Fast feedback needed 97 | - High cyclomatic complexity 98 | 99 | ### Favor Integration Tests When: 100 | 101 | - Testing persistence layer 102 | - Validating service contracts 103 | - Testing middleware/interceptors 104 | - Component boundaries critical 105 | 106 | ### Favor E2E Tests When: 107 | 108 | - User-facing critical paths 109 | - Multi-system interactions 110 | - Regulatory compliance scenarios 111 | - Visual regression important 112 | 113 | ## Anti-patterns to Avoid 114 | 115 | - E2E testing for business logic validation 116 | - Unit testing framework behavior 117 | - Integration testing third-party libraries 118 | - Duplicate coverage across levels 119 | 120 | ## Duplicate Coverage Guard 121 | 122 | **Before adding any test, check:** 123 | 124 | 1. Is this already tested at a lower level? 125 | 2. Can a unit test cover this instead of integration? 126 | 3. Can an integration test cover this instead of E2E? 127 | 128 | **Coverage overlap is only acceptable when:** 129 | 130 | - Testing different aspects (unit: logic, integration: interaction, e2e: user experience) 131 | - Critical paths requiring defense in depth 132 | - Regression prevention for previously broken functionality 133 | 134 | ## Test Naming Conventions 135 | 136 | - Unit: `test_{component}_{scenario}` 137 | - Integration: `test_{flow}_{interaction}` 138 | - E2E: `test_{journey}_{outcome}` 139 | 140 | ## Test ID Format 141 | 142 | `{EPIC}.{STORY}-{LEVEL}-{SEQ}` 143 | 144 | Examples: 145 | 146 | - `1.3-UNIT-001` 147 | - `1.3-INT-002` 148 | - `1.3-E2E-001` 149 | -------------------------------------------------------------------------------- /.bmad-core/tasks/create-doc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Create Document from Template (YAML Driven) 4 | 5 | ## ⚠️ CRITICAL EXECUTION NOTICE ⚠️ 6 | 7 | **THIS IS AN EXECUTABLE WORKFLOW - NOT REFERENCE MATERIAL** 8 | 9 | When this task is invoked: 10 | 11 | 1. **DISABLE ALL EFFICIENCY OPTIMIZATIONS** - This workflow requires full user interaction 12 | 2. **MANDATORY STEP-BY-STEP EXECUTION** - Each section must be processed sequentially with user feedback 13 | 3. **ELICITATION IS REQUIRED** - When `elicit: true`, you MUST use the 1-9 format and wait for user response 14 | 4. **NO SHORTCUTS ALLOWED** - Complete documents cannot be created without following this workflow 15 | 16 | **VIOLATION INDICATOR:** If you create a complete document without user interaction, you have violated this workflow. 17 | 18 | ## Critical: Template Discovery 19 | 20 | If a YAML Template has not been provided, list all templates from .bmad-core/templates or ask the user to provide another. 21 | 22 | ## CRITICAL: Mandatory Elicitation Format 23 | 24 | **When `elicit: true`, this is a HARD STOP requiring user interaction:** 25 | 26 | **YOU MUST:** 27 | 28 | 1. Present section content 29 | 2. Provide detailed rationale (explain trade-offs, assumptions, decisions made) 30 | 3. **STOP and present numbered options 1-9:** 31 | - **Option 1:** Always "Proceed to next section" 32 | - **Options 2-9:** Select 8 methods from data/elicitation-methods 33 | - End with: "Select 1-9 or just type your question/feedback:" 34 | 4. **WAIT FOR USER RESPONSE** - Do not proceed until user selects option or provides feedback 35 | 36 | **WORKFLOW VIOLATION:** Creating content for elicit=true sections without user interaction violates this task. 37 | 38 | **NEVER ask yes/no questions or use any other format.** 39 | 40 | ## Processing Flow 41 | 42 | 1. **Parse YAML template** - Load template metadata and sections 43 | 2. **Set preferences** - Show current mode (Interactive), confirm output file 44 | 3. **Process each section:** 45 | - Skip if condition unmet 46 | - Check agent permissions (owner/editors) - note if section is restricted to specific agents 47 | - Draft content using section instruction 48 | - Present content + detailed rationale 49 | - **IF elicit: true** → MANDATORY 1-9 options format 50 | - Save to file if possible 51 | 4. **Continue until complete** 52 | 53 | ## Detailed Rationale Requirements 54 | 55 | When presenting section content, ALWAYS include rationale that explains: 56 | 57 | - Trade-offs and choices made (what was chosen over alternatives and why) 58 | - Key assumptions made during drafting 59 | - Interesting or questionable decisions that need user attention 60 | - Areas that might need validation 61 | 62 | ## Elicitation Results Flow 63 | 64 | After user selects elicitation method (2-9): 65 | 66 | 1. Execute method from data/elicitation-methods 67 | 2. Present results with insights 68 | 3. Offer options: 69 | - **1. Apply changes and update section** 70 | - **2. Return to elicitation menu** 71 | - **3. Ask any questions or engage further with this elicitation** 72 | 73 | ## Agent Permissions 74 | 75 | When processing sections with agent permission fields: 76 | 77 | - **owner**: Note which agent role initially creates/populates the section 78 | - **editors**: List agent roles allowed to modify the section 79 | - **readonly**: Mark sections that cannot be modified after creation 80 | 81 | **For sections with restricted access:** 82 | 83 | - Include a note in the generated document indicating the responsible agent 84 | - Example: "_(This section is owned by dev-agent and can only be modified by dev-agent)_" 85 | 86 | ## YOLO Mode 87 | 88 | User can type `#yolo` to toggle to YOLO mode (process all sections at once). 89 | 90 | ## CRITICAL REMINDERS 91 | 92 | **❌ NEVER:** 93 | 94 | - Ask yes/no questions for elicitation 95 | - Use any format other than 1-9 numbered options 96 | - Create new elicitation methods 97 | 98 | **✅ ALWAYS:** 99 | 100 | - Use exact 1-9 format when elicit: true 101 | - Select options 2-9 from data/elicitation-methods only 102 | - Provide detailed rationale explaining decisions 103 | - End with "Select 1-9 or just type your question/feedback:" 104 | --------------------------------------------------------------------------------