Files
ggjuku/internal/app/download_directories.go
2026-09-30 10:19:23 +08:00

229 lines
6.0 KiB
Go

package app
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
func dramaSourceFolder(drama Drama) string {
source := accountSourceGroup(firstNonEmpty(drama.Source, sourceFromDramaID(drama.ID)))
if source == "" {
source = accountDramaSource(drama.ID)
}
for _, choice := range accountSourceChoices {
if choice.ID == source {
return safeFilename(choice.Name)
}
}
return "其他来源"
}
func readDownloadDirectoryID(directory string) (string, error) {
path := filepath.Join(directory, ".drama-id")
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
if err != nil {
return "", err
}
if !info.Mode().IsRegular() || info.Size() > 1024 {
return "", errors.New("下载目录标识不是有效的普通文件")
}
body, err := os.ReadFile(path)
return strings.TrimSpace(string(body)), err
}
func scanDownloadDirectories(root string) (map[string]string, error) {
directories := make(map[string]string)
groups := map[string]bool{}
for _, choice := range accountSourceChoices {
groups[safeFilename(choice.Name)] = true
}
var scan func(string, bool) error
scan = func(parent string, grouped bool) error {
entries, err := os.ReadDir(parent)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
continue
}
directory := filepath.Join(parent, entry.Name())
id, err := readDownloadDirectoryID(directory)
if err != nil {
continue
}
if id != "" {
if sourceFromDramaID(id) != sourceHongguo {
continue
}
if previous, exists := directories[id]; exists && previous != directory {
directories[id] = ""
} else if !exists {
directories[id] = directory
}
} else if !grouped && groups[entry.Name()] {
if err := scan(directory, true); err != nil {
return err
}
}
}
return nil
}
err := scan(root, false)
return directories, err
}
func claimDownloadDirectory(directory, id string, known bool) (bool, error) {
if err := os.MkdirAll(directory, 0755); err != nil {
return false, err
}
info, err := os.Lstat(directory)
if err != nil {
return false, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return false, errors.New("下载剧集目录不是普通文件夹")
}
owner, err := readDownloadDirectoryID(directory)
if err != nil || owner != "" {
return owner == id, err
}
if !known {
entries, err := os.ReadDir(directory)
if err != nil {
return false, err
}
if len(entries) > 0 {
return false, nil
}
}
file, err := os.OpenFile(filepath.Join(directory, ".drama-id"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if errors.Is(err, os.ErrExist) {
owner, readErr := readDownloadDirectoryID(directory)
return owner == id, readErr
}
if err != nil {
return false, err
}
_, err = file.WriteString(id)
if closeErr := file.Close(); err == nil {
err = closeErr
}
return err == nil, err
}
func downloadSourceDirectory(root, source string) (string, error) {
if err := os.MkdirAll(root, 0755); err != nil {
return "", err
}
directory := filepath.Join(root, source)
if err := os.Mkdir(directory, 0755); err != nil && !errors.Is(err, os.ErrExist) {
return "", err
}
info, err := os.Lstat(directory)
if err != nil {
return "", err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return "", errors.New("下载站源目录不是普通文件夹")
}
if _, err = os.Lstat(filepath.Join(directory, ".drama-id")); !errors.Is(err, os.ErrNotExist) {
if err != nil {
return "", err
}
return "", fmt.Errorf("站源目录“%s”与已有剧集目录同名,请更换新下载的根目录;原剧集仍使用原目录", source)
}
return directory, nil
}
func (d *Downloader) downloadDramaDirectory(drama Drama, title, existing string) (string, error) {
var valid bool
drama, valid = normalizeHongguoDrama(drama)
if !valid {
return "", errors.New("此版本仅支持红果剧集")
}
d.directoryMu.Lock()
defer d.directoryMu.Unlock()
if drama.ID == "" || len(drama.ID) > 512 || strings.ContainsAny(drama.ID, "\x00\r\n") {
return "", errors.New("下载剧集标识无效")
}
if d.downloadDirectories == nil {
var err error
d.downloadDirectories, err = scanDownloadDirectories(d.cfg.OutputDir)
if err != nil {
d.downloadDirectories = nil
return "", err
}
}
if existing == "" {
var found bool
existing, found = d.downloadDirectories[drama.ID]
if found && existing == "" {
return "", errors.New("发现本剧的多个已有目录,请保留下载任务以确定更新位置")
}
}
if existing != "" {
ok, err := claimDownloadDirectory(existing, drama.ID, true)
if err != nil {
return "", err
}
if !ok {
return "", errors.New("已有下载目录属于其他剧集,原文件已保留")
}
d.downloadDirectories[drama.ID] = existing
return existing, nil
}
root := d.cfg.OutputDir
grouped := d.cfg.GroupBySource
if d.downloadGrouping != nil {
grouped = *d.downloadGrouping
}
if grouped {
var err error
root, err = downloadSourceDirectory(root, dramaSourceFolder(drama))
if err != nil {
return "", err
}
}
for _, name := range []string{title, title + "_" + hashShort(drama.ID)} {
directory, err := safeJoin(root, name)
if err != nil {
return "", err
}
ok, err := claimDownloadDirectory(directory, drama.ID, false)
if err != nil {
return "", err
}
if ok {
d.downloadDirectories[drama.ID] = directory
return directory, nil
}
}
return "", errors.New("下载目录存在同名内容,原文件已保留")
}
func (app *UIApp) dramaDirectoryLocked(id string) string {
if directory := app.dramaDirectories[id]; directory != "" {
return directory
}
for _, taskID := range app.taskOrder {
if task := app.tasks[taskID]; task != nil && task.DramaID == id && !isChapterPlaceholderTask(task) && task.Path != "" {
return filepath.Dir(task.Path)
}
}
if merged := app.merges[id]; merged != nil && merged.OutputPath != "" {
return filepath.Dir(merged.OutputPath)
}
return ""
}