mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
@@ -4,7 +4,21 @@ import { Play, Search } from 'lucide-react'
|
||||
import { imageURL } from '../api/client'
|
||||
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
|
||||
import type { Media } from '../types'
|
||||
import { isTheatricalFeature, seriesTitleFromPath, THEATRICAL_SEASON } from '../utils/groupSeries'
|
||||
import {
|
||||
BONUS_SEASON,
|
||||
EXTRA_SEASON,
|
||||
isTheatricalFeature,
|
||||
NCED_SEASON,
|
||||
NCOP_SEASON,
|
||||
OAD_SEASON,
|
||||
OMAKE_SEASON,
|
||||
ONA_SEASON,
|
||||
OVA_SEASON,
|
||||
OVD_SEASON,
|
||||
PICTURE_DRAMA_SEASON,
|
||||
seriesTitleFromPath,
|
||||
THEATRICAL_SEASON,
|
||||
} from '../utils/groupSeries'
|
||||
import { formatSize } from './libraryPageModel'
|
||||
|
||||
type SeasonGroup = {
|
||||
@@ -135,9 +149,21 @@ export function LibrarySeriesEpisodes({
|
||||
}
|
||||
|
||||
function seasonLabel(season: number): string {
|
||||
if (season === 0) return '特别篇'
|
||||
if (season === THEATRICAL_SEASON) return '剧场版'
|
||||
return `第 ${season} 季`
|
||||
switch (season) {
|
||||
case 0: return '特别篇'
|
||||
case THEATRICAL_SEASON: return '剧场版'
|
||||
case OVA_SEASON: return 'OVA'
|
||||
case OAD_SEASON: return 'OAD'
|
||||
case OVD_SEASON: return 'OVD'
|
||||
case ONA_SEASON: return 'ONA'
|
||||
case EXTRA_SEASON: return 'Extra'
|
||||
case BONUS_SEASON: return 'Bonus'
|
||||
case OMAKE_SEASON: return 'Omake'
|
||||
case PICTURE_DRAMA_SEASON: return 'Picture Drama'
|
||||
case NCOP_SEASON: return 'NCOP'
|
||||
case NCED_SEASON: return 'NCED'
|
||||
default: return `第 ${season} 季`
|
||||
}
|
||||
}
|
||||
|
||||
function episodeDisplayTitle(ep: Media, siblings: Media[]): string {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function seriesSourceRoot(episodes: Media[]): string {
|
||||
if (!firstPath) return ''
|
||||
const dir = dirname(firstPath)
|
||||
const base = basename(dir)
|
||||
if (/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*\d{1,2}\s*季|specials?|sp|ova|oad|extra|extras|特别篇|特別篇|番外|特典)$/i.test(base)) {
|
||||
if (/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*\d{1,2}\s*季|specials?|sp|ovas?|oads?|ovds?|onas?|extras?|bonus(?:es)?|omake|picture[\s._-]*drama|ncop|nced|特别篇|特別篇|番外|特典|画像特典)$/i.test(base)) {
|
||||
return dirname(dir)
|
||||
}
|
||||
return dir
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
import type { Media } from '../types'
|
||||
import { getSeriesKey, isTheatricalFeature, THEATRICAL_SEASON, type SeriesCard } from '../utils/groupSeries'
|
||||
import {
|
||||
BONUS_SEASON,
|
||||
EXTRA_SEASON,
|
||||
getSeriesKey,
|
||||
isTheatricalFeature,
|
||||
NCED_SEASON,
|
||||
NCOP_SEASON,
|
||||
OAD_SEASON,
|
||||
OMAKE_SEASON,
|
||||
ONA_SEASON,
|
||||
OVA_SEASON,
|
||||
OVD_SEASON,
|
||||
PICTURE_DRAMA_SEASON,
|
||||
specialSectionForMedia,
|
||||
THEATRICAL_SEASON,
|
||||
type SeriesCard,
|
||||
} from '../utils/groupSeries'
|
||||
|
||||
type SeasonEpisodes = {
|
||||
season: number
|
||||
@@ -47,9 +63,9 @@ export function useLibrarySeriesSelection({
|
||||
: sourceItems.filter((m) => getSeriesKey(m) === selectedSeries.key)
|
||||
const seasons = new Map<number, Media[]>()
|
||||
for (const ep of eps) {
|
||||
const s = isTheatricalFeature(ep)
|
||||
? THEATRICAL_SEASON
|
||||
: ep.episode_num > 0 ? (ep.season_num ?? 0) : (ep.season_num || 1)
|
||||
const specialSection = specialSectionForMedia(ep)
|
||||
const s = specialSection ??
|
||||
(ep.episode_num > 0 ? (ep.season_num ?? 0) : (ep.season_num || 1))
|
||||
if (!seasons.has(s)) seasons.set(s, [])
|
||||
seasons.get(s)!.push(ep)
|
||||
}
|
||||
@@ -132,7 +148,19 @@ export function useLibrarySeriesSelection({
|
||||
}
|
||||
|
||||
function seasonSortOrder(season: number): number {
|
||||
if (season === 0) return -2
|
||||
if (season === THEATRICAL_SEASON) return -1
|
||||
return season
|
||||
switch (season) {
|
||||
case 0: return 0
|
||||
case OVA_SEASON: return 1
|
||||
case OAD_SEASON: return 2
|
||||
case OVD_SEASON: return 3
|
||||
case ONA_SEASON: return 4
|
||||
case EXTRA_SEASON: return 5
|
||||
case BONUS_SEASON: return 6
|
||||
case OMAKE_SEASON: return 7
|
||||
case PICTURE_DRAMA_SEASON: return 8
|
||||
case NCOP_SEASON: return 9
|
||||
case NCED_SEASON: return 10
|
||||
case THEATRICAL_SEASON: return 11
|
||||
default: return 100 + season
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ export type SeriesCard = {
|
||||
}
|
||||
|
||||
export const THEATRICAL_SEASON = -1
|
||||
export const OVA_SEASON = -2
|
||||
export const OAD_SEASON = -3
|
||||
export const OVD_SEASON = -4
|
||||
export const ONA_SEASON = -5
|
||||
export const EXTRA_SEASON = -6
|
||||
export const BONUS_SEASON = -7
|
||||
export const OMAKE_SEASON = -8
|
||||
export const PICTURE_DRAMA_SEASON = -9
|
||||
export const NCOP_SEASON = -10
|
||||
export const NCED_SEASON = -11
|
||||
|
||||
export function getSeriesKey(media: Media): string {
|
||||
return compactSeriesKey(getSeriesRawKey(media))
|
||||
@@ -92,16 +102,19 @@ export function isEpisodeLike(media: Media): boolean {
|
||||
// 剧集类目录名(电视剧/动漫及其二级分类)。媒体路径落在这些目录下时, 即便
|
||||
// 季集号未识别出来, 也应按剧集对待, 跳转到 /library 分类视图而非 /media 单页。
|
||||
const EPISODIC_PATH_RE =
|
||||
/[\\/](?:电视剧|剧集|连续剧|短剧|国产剧|国剧|大陆剧|华语剧|国产电视剧|大陆电视剧|华语电视剧|欧美剧|欧美电视剧|美剧|英剧|日韩剧|日韩电视剧|日剧|韩剧|港剧|台剧|港台剧|泰剧|综艺|纪录片|儿童|动漫|番剧|国漫|日番|韩漫|美漫|欧美动漫|欧美动画|其他动漫|anime|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)[\\/]/i
|
||||
/[\\/](?:电视剧|剧集|连续剧|短剧|国产剧|国剧|大陆剧|华语剧|国产电视剧|大陆电视剧|华语电视剧|欧美剧|欧美电视剧|美剧|英剧|日韩剧|日韩电视剧|日剧|韩剧|港剧|台剧|港台剧|泰剧|综艺|纪录片|儿童|动漫|番剧|国漫|日番|韩漫|美漫|欧美动漫|欧美动画|其他动漫|anime|tv|series|shows?|season[\s._-]*\d|s\d{1,2}(?:[\s._-]|[\\/])|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|ovds?|onas?|extras?|bonus(?:es)?|omake|picture[\s._-]*drama|ncop|nced|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇|画像特典)[\\/]/i
|
||||
|
||||
const THEATRICAL_TITLE_RE =
|
||||
/(?:剧场版|劇場版|动画电影|動畫電影|电影版|電影版|\bthe\s+movie\b|\bmovie\s*\d{1,2}\b)/i
|
||||
|
||||
const THEATRICAL_FOLDER_NAME_RE =
|
||||
/(?:剧场版|劇場版|动画电影|動畫電影|电影版|電影版)/i
|
||||
|
||||
const THEATRICAL_FOLDER_RE =
|
||||
/[\\/](?:剧场版|劇場版|动画电影|動畫電影)[\\/]/
|
||||
/[\\/][^\\/]*(?:剧场版|劇場版|动画电影|動畫電影|电影版|電影版)[^\\/]*[\\/]/
|
||||
|
||||
const SEASON_FOLDER_RE =
|
||||
/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*[0-9一二三四五六七八九十百零两]+\s*季|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|extras?|bonus(?:es)?|omake|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇|剧场版|劇場版|动画电影|動畫電影)$/i
|
||||
/^(?:s\d{1,2}|season[\s._-]*\d{1,2}|第\s*[0-9一二三四五六七八九十百零两]+\s*季|special[\s._-]*episodes?|specials?|sp|ovas?|oads?|ovds?|onas?|extras?|bonus(?:es)?|omake|picture[\s._-]*drama|ncop|nced|特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇|画像特典|剧场版|劇場版|动画电影|動畫電影)$/i
|
||||
|
||||
export function pathLooksEpisodic(media: Media): boolean {
|
||||
const path = (media.path || media.display_library_path || media.library_path || '')
|
||||
@@ -114,6 +127,30 @@ export function isTheatricalFeature(media: Media): boolean {
|
||||
return THEATRICAL_TITLE_RE.test(`${media.title || ''} ${path}`) || THEATRICAL_FOLDER_RE.test(path)
|
||||
}
|
||||
|
||||
const SPECIAL_SECTION_PATTERNS: Array<[number, RegExp]> = [
|
||||
[OVA_SEASON, /(?:^|[^a-z0-9])ovas?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[OAD_SEASON, /(?:^|[^a-z0-9])oads?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[OVD_SEASON, /(?:^|[^a-z0-9])ovds?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[ONA_SEASON, /(?:^|[^a-z0-9])onas?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[PICTURE_DRAMA_SEASON, /(?:^|[^a-z0-9])(?:picture[\s._-]*drama|画像特典)(?:[^a-z0-9]|$)/i],
|
||||
[NCOP_SEASON, /(?:^|[^a-z0-9])ncop(?:\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[NCED_SEASON, /(?:^|[^a-z0-9])nced(?:\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[EXTRA_SEASON, /(?:^|[^a-z0-9])extras?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[BONUS_SEASON, /(?:^|[^a-z0-9])bonus(?:es)?(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
[OMAKE_SEASON, /(?:^|[^a-z0-9])omake(?:[\s._-]*\d+)?(?:[^a-z0-9]|$)/i],
|
||||
]
|
||||
|
||||
export function specialSectionForMedia(media: Media): number | null {
|
||||
if (isTheatricalFeature(media)) return THEATRICAL_SEASON
|
||||
const parts = (media.path || '').split(/[\\/]+/).filter(Boolean)
|
||||
for (const part of parts) {
|
||||
for (const [section, pattern] of SPECIAL_SECTION_PATTERNS) {
|
||||
if (pattern.test(part)) return section
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function isSeriesCard(card: SeriesCard): boolean {
|
||||
return (
|
||||
card.count > 1 ||
|
||||
@@ -170,10 +207,10 @@ function normalizeTitle(value?: string): string {
|
||||
}
|
||||
|
||||
const SERIES_SPECIAL_CODE_RE =
|
||||
/\s*[[((【]?\s*(?:s0+\s*e?\s*\d+|season\s*0+(?:\s*episode)?\s*\d*|special(?:\s*episode)?s?\s*\d*|sp\s*\d*|ovas?\s*\d*|oads?\s*\d*|extras?\s*\d*|bonus(?:es)?\s*\d*|omake\s*\d*)\s*[\]))】]?$/i
|
||||
/\s*[[((【]?\s*(?:s0+\s*e?\s*\d+|season\s*0+(?:\s*episode)?\s*\d*|special(?:\s*episode)?s?\s*\d*|sp\s*\d*|ovas?\s*\d*|oads?\s*\d*|ovds?\s*\d*|onas?\s*\d*|extras?\s*\d*|bonus(?:es)?\s*\d*|omake\s*\d*|picture[\s._-]*drama\s*\d*|ncop\s*\d*|nced\s*\d*)\s*[\]))】]?$/i
|
||||
|
||||
const SERIES_SPECIAL_CJK_RE =
|
||||
/\s*[[((【]?\s*(?:特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇)(?:\s*第?\s*[0-9一二三四五六七八九十百零两]+(?:[集话話期])?)?\s*[\]))】]?$/i
|
||||
/\s*[[((【]?\s*(?:特别篇|特別篇|番外篇?|特典|外传|外傳|总集篇|總集篇|画像特典)(?:\s*第?\s*[0-9一二三四五六七八九十百零两]+(?:[集话話期])?)?\s*[\]))】]?$/i
|
||||
|
||||
function normalizePathSeriesTitle(value?: string): string {
|
||||
const title = normalizeTitle(value)
|
||||
@@ -271,16 +308,20 @@ function seriesDirectoryNameFromPath(path?: string): string {
|
||||
if (parts.length < 2) return ''
|
||||
let dirIndex = parts.length - 2
|
||||
const lastPart = parts[parts.length - 1]
|
||||
if (!seriesPathPartLooksLikeFile(lastPart) && !SEASON_FOLDER_RE.test(lastPart)) {
|
||||
if (!seriesPathPartLooksLikeFile(lastPart) && !isSeriesContainerFolder(lastPart)) {
|
||||
dirIndex = parts.length - 1
|
||||
}
|
||||
while (dirIndex >= 0 && SEASON_FOLDER_RE.test(parts[dirIndex])) {
|
||||
while (dirIndex >= 0 && isSeriesContainerFolder(parts[dirIndex])) {
|
||||
dirIndex -= 1
|
||||
}
|
||||
if (dirIndex < 0) return ''
|
||||
return parts[dirIndex]
|
||||
}
|
||||
|
||||
function isSeriesContainerFolder(name: string): boolean {
|
||||
return SEASON_FOLDER_RE.test(name) || THEATRICAL_FOLDER_NAME_RE.test(name)
|
||||
}
|
||||
|
||||
function seriesExternalIDFromPath(path?: string): string {
|
||||
const part = seriesDirectoryNameFromPath(path)
|
||||
if (!part) return ''
|
||||
|
||||
Reference in New Issue
Block a user