From 6d1687e51998d707fbbca72cdfe18aee7e08e7dd Mon Sep 17 00:00:00 2001 From: tickstep Date: Tue, 18 Feb 2025 14:10:13 +0800 Subject: [PATCH] add album command --- internal/command/album.go | 572 +++---------- internal/command/album_web.go | 786 ++++++++++++++++++ internal/command/download.go | 2 + internal/file/downloader/downloader.go | 99 ++- .../pandownload/download_task_unit.go | 28 +- internal/global/pan_file.go | 12 + main.go | 6 +- 7 files changed, 1020 insertions(+), 485 deletions(-) create mode 100644 internal/command/album_web.go create mode 100644 internal/global/pan_file.go diff --git a/internal/command/album.go b/internal/command/album.go index bdc031e..2eb71f4 100644 --- a/internal/command/album.go +++ b/internal/command/album.go @@ -14,46 +14,36 @@ package command import ( + "encoding/json" "fmt" "github.com/olekukonko/tablewriter" "github.com/tickstep/aliyunpan-api/aliyunpan" - "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" - "github.com/tickstep/aliyunpan-api/aliyunpan_web" "github.com/tickstep/aliyunpan/cmder" "github.com/tickstep/aliyunpan/cmder/cmdtable" "github.com/tickstep/aliyunpan/internal/config" "github.com/tickstep/aliyunpan/internal/file/downloader" "github.com/tickstep/aliyunpan/internal/functions/pandownload" + "github.com/tickstep/aliyunpan/internal/global" "github.com/tickstep/aliyunpan/internal/taskframework" "github.com/tickstep/aliyunpan/internal/utils" "github.com/tickstep/aliyunpan/library/requester/transfer" "github.com/tickstep/library-go/converter" "github.com/tickstep/library-go/logger" + "github.com/tickstep/library-go/requester" "github.com/tickstep/library-go/requester/rio/speeds" "github.com/urfave/cli" "os" "path/filepath" "strconv" - "sync/atomic" + "strings" "time" ) -type ( - AlbumFileCategoryOption string -) - -var ( - ImageOnlyOption AlbumFileCategoryOption = "image" - VideoOnlyOption AlbumFileCategoryOption = "video" - ImageVideoOnlyOption AlbumFileCategoryOption = "image_video" - AllFileOption AlbumFileCategoryOption = "none" -) - func CmdAlbum() cli.Command { return cli.Command{ Name: "album", Aliases: []string{"abm"}, - Usage: "个人相册(Beta)", + Usage: "共享相册", UsageText: cmder.App().Name + " album", Category: "阿里云盘", Before: ReloadConfigFunc, @@ -66,12 +56,11 @@ func CmdAlbum() cli.Command { { Name: "list", Aliases: []string{"ls"}, - Usage: "展示相簿列表", + Usage: "展示共享相簿列表", UsageText: cmder.App().Name + " album list", Description: ` 示例: - - 展示相簿列表 + 展示共享相簿列表 aliyunpan album ls `, Action: func(c *cli.Context) error { @@ -79,94 +68,7 @@ func CmdAlbum() cli.Command { fmt.Println("未登录账号") return nil } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - RunAlbumList() - return nil - }, - Flags: []cli.Flag{}, - }, - { - Name: "new", - Aliases: []string{""}, - Usage: "创建相簿", - UsageText: cmder.App().Name + " album new", - Description: ` -示例: - - 新建相簿,名称为:我的相簿2022 - aliyunpan album new "我的相簿2022" - - 新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件 - aliyunpan album new "我的相簿2022" "存放2022所有文件" -`, - Action: func(c *cli.Context) error { - if config.Config.ActiveUser() == nil { - fmt.Println("未登录账号") - return nil - } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - RunAlbumCreate(c.Args().Get(0), c.Args().Get(1)) - return nil - }, - Flags: []cli.Flag{}, - }, - { - Name: "rm", - Aliases: []string{""}, - Usage: "删除相簿", - UsageText: cmder.App().Name + " album rm", - Description: ` -删除相簿,同名的相簿只会删除第一个符合条件的 -示例: - - 删除名称为"我的相簿2022"的相簿 - aliyunpan album rm "我的相簿2022" - - 删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿 - aliyunpan album rm "我的相簿2022-1" "我的相簿2022-2" -`, - Action: func(c *cli.Context) error { - if config.Config.ActiveUser() == nil { - fmt.Println("未登录账号") - return nil - } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - RunAlbumDelete(c.Args()) - return nil - }, - Flags: []cli.Flag{}, - }, - { - Name: "rename", - Aliases: []string{""}, - Usage: "重命名相簿", - UsageText: cmder.App().Name + " album rename", - Description: ` -重命名相簿,同名的相簿只会修改第一个符合条件的 -示例: - - 重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new" - aliyunpan album rename "我的相簿2022" "我的相簿2022-new" -`, - Action: func(c *cli.Context) error { - if config.Config.ActiveUser() == nil { - fmt.Println("未登录账号") - return nil - } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - RunAlbumRename(c.Args().Get(0), c.Args().Get(1)) + RunShareAlbumList() return nil }, Flags: []cli.Flag{}, @@ -188,76 +90,7 @@ func CmdAlbum() cli.Command { fmt.Println("未登录账号") return nil } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - RunAlbumListFile(c.Args().Get(0)) - return nil - }, - Flags: []cli.Flag{}, - }, - { - Name: "rm-file", - Aliases: []string{"rf"}, - Usage: "移除相簿中的文件", - UsageText: cmder.App().Name + " album rm-file", - Description: ` -移除相簿中的文件,同名的相簿只会移除第一个符合条件的 -示例: - - 移除相簿 "我的相簿2022" 中的文件 1.png 2.png - aliyunpan album rm-file 我的相簿2022 1.png 2.png -`, - Action: func(c *cli.Context) error { - if config.Config.ActiveUser() == nil { - fmt.Println("未登录账号") - return nil - } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - subArgs := c.Args() - if len(subArgs) < 2 { - fmt.Println("请指定移除的文件") - return nil - } - RunAlbumRmFile(subArgs[0], subArgs[1:]) - return nil - }, - Flags: []cli.Flag{}, - }, - { - Name: "add-file", - Aliases: []string{"af"}, - Usage: "增加(文件/相册)网盘文件到相簿中", - UsageText: cmder.App().Name + " album add-file", - Description: ` -增加文件到相簿中 -示例: - - 增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中 - aliyunpan album add-file 我的相簿2022 1.png 2.png - - 增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中 - aliyunpan album add-file 我的相簿2022 myFolder -`, - Action: func(c *cli.Context) error { - if config.Config.ActiveUser() == nil { - fmt.Println("未登录账号") - return nil - } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } - subArgs := c.Args() - if len(subArgs) < 2 { - fmt.Println("请指定增加的文件") - return nil - } - RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption) + RunShareAlbumListFile(c.Args().Get(0)) return nil }, Flags: []cli.Flag{}, @@ -273,17 +106,12 @@ func CmdAlbum() cli.Command { 下载相簿 "我的相簿2022" 里面的所有文件 aliyunpan album download-file 我的相簿2022 - `, Action: func(c *cli.Context) error { if config.Config.ActiveUser() == nil { fmt.Println("未登录账号") return nil } - if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { - fmt.Println("WEB客户端未登录,请登录后再使用该命令") - return nil - } subArgs := c.Args() if len(subArgs) == 0 { fmt.Println("请指定下载的相簿名称") @@ -312,7 +140,7 @@ func CmdAlbum() cli.Command { ExcludeNames: []string{}, } - RunAlbumDownloadFile(c.Args(), do) + RunShareAlbumDownloadFile(c.Args(), do) return nil }, Flags: []cli.Flag{ @@ -334,76 +162,25 @@ func CmdAlbum() cli.Command { } } -func RunAlbumList() { +func RunShareAlbumList() { activeUser := GetActiveUser() - records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) + records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll() if err != nil { fmt.Printf("获取相簿列表失败: %s\n", err) return } tb := cmdtable.NewTable(os.Stdout) - tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"}) - tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT}) + tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "更新日期", "创建日期"}) + tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT}) for k, record := range records { - tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount), - record.CreatedAtStr(), record.UpdatedAtStr()}) + tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, record.UpdatedAtStr(), record.CreatedAtStr()}) } tb.Render() } -func RunAlbumCreate(name, description string) { - if name == "" { - fmt.Printf("相簿名称不能为空\n") - return - } - - activeUser := GetActiveUser() - _, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{ - Name: name, - Description: description, - }) - if err != nil { - fmt.Printf("创建相簿失败: %s\n", err) - return - } - fmt.Printf("创建相簿成功: %s\n", name) -} - -func RunAlbumDelete(nameList []string) { - if len(nameList) == 0 { - fmt.Printf("相簿名称不能为空\n") - return - } - - activeUser := GetActiveUser() - records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) - if err != nil { - fmt.Printf("获取相簿列表失败: %s\n", err) - return - } - - for _, record := range records { - for i, name := range nameList { - if name == record.Name { - nameList = append(nameList[:i], nameList[i+1:]...) - _, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{ - AlbumId: record.AlbumId, - }) - if err != nil { - fmt.Printf("删除相簿失败: %s\n", name) - return - } else { - fmt.Printf("删除相簿成功: %s\n", name) - } - break - } - } - } -} - -func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity { - records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) +func getShareAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity { + records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll() if err != nil { fmt.Printf("获取相簿列表失败: %s\n", err) return nil @@ -417,48 +194,21 @@ func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumE return nil } -func RunAlbumRename(name, newName string) { - if len(name) == 0 { - fmt.Printf("相簿名称不能为空\n") - return - } - if len(newName) == 0 { - fmt.Printf("相簿名称不能为空\n") - return - } - - activeUser := GetActiveUser() - record := getAlbumFromName(activeUser, name) - if record == nil { - return - } - _, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{ - AlbumId: record.AlbumId, - Description: record.Description, - Name: newName, - }) - if err != nil { - fmt.Printf("重命名相簿失败: %s\n", name) - return - } else { - fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName) - } -} - -func RunAlbumListFile(name string) { +func RunShareAlbumListFile(name string) { if len(name) == 0 { fmt.Printf("相簿名称不能为空\n") return } activeUser := GetActiveUser() - record := getAlbumFromName(activeUser, name) + record := getShareAlbumFromName(activeUser, name) if record == nil { return } - fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ + fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{ AlbumId: record.AlbumId, + Limit: 100, }) if er != nil { fmt.Printf("获取相簿文件列表失败:%s\n", er) @@ -467,174 +217,13 @@ func RunAlbumListFile(name string) { renderTable(opLs, false, "", fileList) } -func RunAlbumRmFile(name string, nameList []string) { - if len(name) == 0 { - fmt.Printf("相簿名称不能为空\n") - return - } - if len(nameList) == 0 { - fmt.Printf("指定文件不能为空\n") - return - } - - activeUser := GetActiveUser() - album := getAlbumFromName(activeUser, name) - if album == nil { - return - } - - fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ - AlbumId: album.AlbumId, - }) - if er != nil { - fmt.Printf("获取相簿文件列表失败:%s\n", er) - return - } - param := &aliyunpan_web.AlbumDeleteFileParam{ - AlbumId: album.AlbumId, - DriveFileList: []aliyunpan.FileBatchActionParam{}, - } - for _, file := range fileList { - if len(nameList) == 0 { - break - } - for i, name := range nameList { - if name == file.FileName { - nameList = append(nameList[:i], nameList[i+1:]...) - param.AddFileItem(file.DriveId, file.FileId) - break - } - } - } - - // 1-500 范围 - if len(param.DriveFileList) == 0 { - fmt.Printf("没有符合的文件\n") - return - } - // delete file - _, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param) - if e != nil { - fmt.Printf("删除相簿文件失败:%s\n", e) - return - } - fmt.Printf("删除相簿文件成功:%s\n", name) -} - -// RunAlbumAddFile 增加网盘文件到相簿 -func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) { - activeUser := GetActiveUser() - - if albumName == "" { - fmt.Printf("必须指定相簿名称\n") - return - } - album := getAlbumFromName(activeUser, albumName) - if album == nil { - fmt.Printf("相簿不存在\n") - return - } - - paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...) - if err != nil { - fmt.Println(err) - return - } - if len(paths) == 0 { - fmt.Printf("没有有效的文件\n") - return - } - - fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n") - param := &aliyunpan_web.AlbumAddFileParam{ - AlbumId: album.AlbumId, - DriveFileList: []aliyunpan.FileBatchActionParam{}, - } - for k := range paths { - filePath := paths[k] - fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath) - if apierr != nil { - fmt.Printf("获取文件信息失败: %s\n", filePath) - continue - } - if fileInfo.IsFile() { - // file - if isFileMatchCondition(fileInfo, filterOption) { - param.AddFileItem(fileInfo.DriveId, fileInfo.FileId) - } - } else { - // folder - activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool { - if apiError != nil { - logger.Verbosef("%s\n", apiError) - return true - } - if !fd.IsFolder() { - if isFileMatchCondition(fd, filterOption) { - param.AddFileItem(fd.DriveId, fd.FileId) - } - } - time.Sleep(2 * time.Second) - return true - }) - } - time.Sleep(2 * time.Second) - } - - if len(param.DriveFileList) == 0 { - fmt.Printf("没有符合的文件\n") - return - } - // add file - _, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param) - if e != nil { - fmt.Printf("增加相簿文件失败:%s\n", e) - return - } - fmt.Printf("增加相簿文件成功:%s\n", albumName) -} - -func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool { - if fileInfo == nil { - return false - } - if filterOption == ImageOnlyOption { - return fileInfo.Category == "image" - } else if filterOption == VideoOnlyOption { - return fileInfo.Category == "video" - } else if filterOption == ImageVideoOnlyOption { - return fileInfo.Category == "image" || fileInfo.Category == "video" - } else if filterOption == AllFileOption { - return true - } - return false -} - -func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { +func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) { if len(albumNames) == 0 { - fmt.Printf("相簿名称不能为空\n") + fmt.Printf("请指定相簿名称\n") return } activeUser := GetActiveUser() - activeUser.PanClient().WebapiPanClient().EnableCache() - activeUser.PanClient().WebapiPanClient().ClearCache() - defer activeUser.PanClient().WebapiPanClient().DisableCache() - // pan token expired checker - continueFlag := int32(0) - atomic.StoreInt32(&continueFlag, 0) - defer func() { - atomic.StoreInt32(&continueFlag, 1) - }() - go func(flag *int32) { - for atomic.LoadInt32(flag) == 0 { - time.Sleep(time.Duration(1) * time.Minute) - if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) { - logger.Verboseln("update access token for download task") - } - } - }(&continueFlag) - if options == nil { options = &DownloadOptions{} } @@ -668,6 +257,10 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { if options.Parallel > config.MaxFileDownloadParallelNum { options.Parallel = config.MaxFileDownloadParallelNum } + // 设置单个文件下载分片线程数 + if options.SliceParallel < 1 { + options.SliceParallel = 1 + } // 保存文件的本地根文件夹 originSaveRootPath := "" @@ -693,6 +286,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { panClient = activeUser.PanClient() ) cfg.MaxParallel = options.Parallel + cfg.SliceParallel = options.SliceParallel var ( executor = taskframework.TaskExecutor{ @@ -707,14 +301,26 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { globalSpeedsStat := &speeds.Speeds{} // 处理队列 + allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll() + if err != nil { + fmt.Printf("获取相簿列表失败: %s\n", err) + return + } for k := range albumNames { - record := getAlbumFromName(activeUser, albumNames[k]) + var record *aliyunpan.AlbumEntity + for _, album := range allShareAlbumList { + if album.Name == albumNames[k] { + record = album + break + } + } if record == nil { continue } // 获取相簿下的所有文件 - fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ + fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{ AlbumId: record.AlbumId, + Limit: 100, }) if er != nil { fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k]) @@ -724,9 +330,61 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { fmt.Printf("相簿里面没有文件: %s\n", albumNames[k]) continue } - for _, f := range fileList { - // 补全虚拟网盘路径,规则:/<相簿名称>/文件名称 - f.Path = "/" + albumNames[k] + "/" + f.FileName + + // 相薄文件是没有子文件夹的,这个fileList就是该相册里面的全部文件了,所以不需要再遍历子文件夹了 + idx := 0 + for { + if idx >= len(fileList) { + break + } + f := fileList.Item(idx) + idx += 1 + // 处理实况照片 + if f.IsAlbumLivePhotoFile() { + // 如果是实况照片,则需要下载图片+视频两个文件 + // 获取下载链接 + durl, apierr := activeUser.PanClient().OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{ + AlbumId: f.AlbumId, + DriveId: f.DriveId, + FileId: f.FileId, + }) + if apierr != nil { + logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId) + fmt.Printf("\n下载照片失败: %s\n", f.FileName) + continue + } + if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接 + // 照片文件 + photoFile := cloneFileEntity(f) + photoFileSize := int64(0) + if durl.StreamsUrl.Heic != "" { + beforeStr, _ := strings.CutSuffix(f.FileName, ".livp") + photoFile.FileName = beforeStr + ".HEIC" + photoFile.FileExtension = "heic" + photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Heic) + } else if durl.StreamsUrl.Jpeg != "" { + beforeStr, _ := strings.CutSuffix(f.FileName, ".livp") + photoFile.FileName = beforeStr + ".JPG" + photoFile.FileExtension = "jpg" + photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Jpeg) + } + photoFile.FileSize = photoFileSize + fileList = append(fileList, photoFile) + + // 视频文件 + videoFile := cloneFileEntity(f) + videoFile.FileSize = f.FileSize - photoFileSize + if durl.StreamsUrl.Mov != "" { + beforeStr, _ := strings.CutSuffix(f.FileName, ".livp") + videoFile.FileName = beforeStr + ".MOV" + videoFile.FileExtension = "mov" + } + fileList = append(fileList, videoFile) + } + continue + } + // 补全虚拟网盘路径,规则:/共享相册/<相簿名称>/文件名称 + f.Path = "/共享相册/" + albumNames[k] + "/" + f.FileName // 生成下载项 newCfg := *cfg @@ -741,15 +399,15 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { IsExecutedPermission: options.IsExecutedPermission, IsOverwrite: options.IsOverwrite, NoCheck: options.NoCheck, + FilePanSource: global.AlbumSource, FilePanPath: f.Path, - DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘) + DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘) GlobalSpeedsStat: globalSpeedsStat, FileRecorder: nil, } - // TODO: 相册下载需要重构 // 设置相簿文件信息 - //unit.SetFileInfo(pandownload.AlbumFileSource, f) + unit.SetFileInfo(global.AlbumSource, f) // 设置储存的路径 if options.SaveTo != "" { @@ -785,3 +443,33 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { tb.Render() } } + +func cloneFileEntity(entity *aliyunpan.FileEntity) *aliyunpan.FileEntity { + data, err := json.Marshal(entity) // 序列化原始对象 + if err != nil { + panic(err) + } + newEntity := &aliyunpan.FileEntity{} + err = json.Unmarshal(data, newEntity) // 反序列化到新的对象中 + if err != nil { + panic(err) + } + return newEntity +} + +func getHttpDownloadFileSize(fileUrl string) int64 { + client := requester.NewHTTPClient() + client.SetKeepAlive(true) + client.SetTimeout(10 * time.Minute) + // header + headers := map[string]string{ + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "referer": "https://www.aliyundrive.com/", + } + resp, err := client.Req("GET", fileUrl, nil, headers) + if err != nil { + return -1 + } + fileSize := resp.ContentLength + return fileSize +} diff --git a/internal/command/album_web.go b/internal/command/album_web.go new file mode 100644 index 0000000..faf3799 --- /dev/null +++ b/internal/command/album_web.go @@ -0,0 +1,786 @@ +// Copyright (c) 2020 tickstep. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package command + +import ( + "fmt" + "github.com/olekukonko/tablewriter" + "github.com/tickstep/aliyunpan-api/aliyunpan" + "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" + "github.com/tickstep/aliyunpan-api/aliyunpan_web" + "github.com/tickstep/aliyunpan/cmder" + "github.com/tickstep/aliyunpan/cmder/cmdtable" + "github.com/tickstep/aliyunpan/internal/config" + "github.com/tickstep/aliyunpan/internal/file/downloader" + "github.com/tickstep/aliyunpan/internal/functions/pandownload" + "github.com/tickstep/aliyunpan/internal/taskframework" + "github.com/tickstep/aliyunpan/internal/utils" + "github.com/tickstep/aliyunpan/library/requester/transfer" + "github.com/tickstep/library-go/converter" + "github.com/tickstep/library-go/logger" + "github.com/tickstep/library-go/requester/rio/speeds" + "github.com/urfave/cli" + "os" + "path/filepath" + "strconv" + "sync/atomic" + "time" +) + +type ( + AlbumFileCategoryOption string +) + +var ( + ImageOnlyOption AlbumFileCategoryOption = "image" + VideoOnlyOption AlbumFileCategoryOption = "video" + ImageVideoOnlyOption AlbumFileCategoryOption = "image_video" + AllFileOption AlbumFileCategoryOption = "none" +) + +func CmdAlbumWeb() cli.Command { + return cli.Command{ + Name: "albumw", + Usage: "个人相册", + UsageText: cmder.App().Name + " albumw", + Category: "阿里云盘", + Before: ReloadConfigFunc, + Action: func(c *cli.Context) error { + cli.ShowCommandHelp(c, c.Command.Name) + return nil + }, + + Subcommands: []cli.Command{ + { + Name: "list", + Aliases: []string{"ls"}, + Usage: "展示相簿列表", + UsageText: cmder.App().Name + " album list", + Description: ` +示例: + + 展示相簿列表 + aliyunpan albumw ls +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + RunAlbumList() + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "new", + Aliases: []string{""}, + Usage: "创建相簿", + UsageText: cmder.App().Name + " album new", + Description: ` +示例: + + 新建相簿,名称为:我的相簿2022 + aliyunpan albumw new "我的相簿2022" + + 新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件 + aliyunpan albumw new "我的相簿2022" "存放2022所有文件" +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + RunAlbumCreate(c.Args().Get(0), c.Args().Get(1)) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "rm", + Aliases: []string{""}, + Usage: "删除相簿", + UsageText: cmder.App().Name + " album rm", + Description: ` +删除相簿,同名的相簿只会删除第一个符合条件的 +示例: + + 删除名称为"我的相簿2022"的相簿 + aliyunpan albumw rm "我的相簿2022" + + 删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿 + aliyunpan albumw rm "我的相簿2022-1" "我的相簿2022-2" +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + RunAlbumDelete(c.Args()) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "rename", + Aliases: []string{""}, + Usage: "重命名相簿", + UsageText: cmder.App().Name + " album rename", + Description: ` +重命名相簿,同名的相簿只会修改第一个符合条件的 +示例: + + 重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new" + aliyunpan albumw rename "我的相簿2022" "我的相簿2022-new" +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + RunAlbumRename(c.Args().Get(0), c.Args().Get(1)) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "list-file", + Aliases: []string{"lf"}, + Usage: "展示相簿中的文件", + UsageText: cmder.App().Name + " album list-file", + Description: ` +展示相簿中文件,同名的相簿只会展示第一个符合条件的 +示例: + + 展示相簿中文件"我的相簿2022" + aliyunpan albumw list-file "我的相簿2022" +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + RunAlbumListFile(c.Args().Get(0)) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "rm-file", + Aliases: []string{"rf"}, + Usage: "移除相簿中的文件", + UsageText: cmder.App().Name + " album rm-file", + Description: ` +移除相簿中的文件,同名的相簿只会移除第一个符合条件的 +示例: + + 移除相簿 "我的相簿2022" 中的文件 1.png 2.png + aliyunpan albumw rm-file 我的相簿2022 1.png 2.png +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + subArgs := c.Args() + if len(subArgs) < 2 { + fmt.Println("请指定移除的文件") + return nil + } + RunAlbumRmFile(subArgs[0], subArgs[1:]) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "add-file", + Aliases: []string{"af"}, + Usage: "增加(文件/相册)网盘文件到相簿中", + UsageText: cmder.App().Name + " album add-file", + Description: ` +增加文件到相簿中 +示例: + + 增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中 + aliyunpan albumw add-file 我的相簿2022 1.png 2.png + + 增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中 + aliyunpan albumw add-file 我的相簿2022 myFolder +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + subArgs := c.Args() + if len(subArgs) < 2 { + fmt.Println("请指定增加的文件") + return nil + } + RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption) + return nil + }, + Flags: []cli.Flag{}, + }, + { + Name: "download-file", + Aliases: []string{"df"}, + Usage: "下载相簿中的所有文件到本地", + UsageText: cmder.App().Name + " album download-file", + Description: ` +下载相簿中的所有文件 +示例: + + 下载相簿 "我的相簿2022" 里面的所有文件 + aliyunpan albumw download-file 我的相簿2022 + +`, + Action: func(c *cli.Context) error { + if config.Config.ActiveUser() == nil { + fmt.Println("未登录账号") + return nil + } + if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil { + fmt.Println("WEB客户端未登录,请登录后再使用该命令") + return nil + } + subArgs := c.Args() + if len(subArgs) == 0 { + fmt.Println("请指定下载的相簿名称") + return nil + } + + // 处理saveTo + var ( + saveTo string + ) + if c.String("saveto") != "" { + saveTo = filepath.Clean(c.String("saveto")) + } + + do := &DownloadOptions{ + IsPrintStatus: false, + IsExecutedPermission: false, + IsOverwrite: c.Bool("ow"), + SaveTo: saveTo, + Parallel: 0, + Load: 0, + MaxRetry: pandownload.DefaultDownloadMaxRetry, + NoCheck: false, + ShowProgress: !c.Bool("np"), + DriveId: parseDriveId(c), + ExcludeNames: []string{}, + } + + RunAlbumDownloadFile(c.Args(), do) + return nil + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "ow", + Usage: "overwrite, 覆盖已存在的文件", + }, + cli.StringFlag{ + Name: "saveto", + Usage: "将下载的文件直接保存到指定的目录", + }, + cli.BoolFlag{ + Name: "np", + Usage: "no progress 不展示下载进度条", + }, + }, + }, + }, + } +} + +func RunAlbumList() { + activeUser := GetActiveUser() + records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) + if err != nil { + fmt.Printf("获取相簿列表失败: %s\n", err) + return + } + + tb := cmdtable.NewTable(os.Stdout) + tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"}) + tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT}) + for k, record := range records { + tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount), + record.CreatedAtStr(), record.UpdatedAtStr()}) + } + tb.Render() +} + +func RunAlbumCreate(name, description string) { + if name == "" { + fmt.Printf("相簿名称不能为空\n") + return + } + + activeUser := GetActiveUser() + _, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{ + Name: name, + Description: description, + }) + if err != nil { + fmt.Printf("创建相簿失败: %s\n", err) + return + } + fmt.Printf("创建相簿成功: %s\n", name) +} + +func RunAlbumDelete(nameList []string) { + if len(nameList) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + + activeUser := GetActiveUser() + records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) + if err != nil { + fmt.Printf("获取相簿列表失败: %s\n", err) + return + } + + for _, record := range records { + for i, name := range nameList { + if name == record.Name { + nameList = append(nameList[:i], nameList[i+1:]...) + _, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{ + AlbumId: record.AlbumId, + }) + if err != nil { + fmt.Printf("删除相簿失败: %s\n", name) + return + } else { + fmt.Printf("删除相簿成功: %s\n", name) + } + break + } + } + } +} + +func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity { + records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{}) + if err != nil { + fmt.Printf("获取相簿列表失败: %s\n", err) + return nil + } + + for _, record := range records { + if name == record.Name { + return record + } + } + return nil +} + +func RunAlbumRename(name, newName string) { + if len(name) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + if len(newName) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + + activeUser := GetActiveUser() + record := getAlbumFromName(activeUser, name) + if record == nil { + return + } + _, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{ + AlbumId: record.AlbumId, + Description: record.Description, + Name: newName, + }) + if err != nil { + fmt.Printf("重命名相簿失败: %s\n", name) + return + } else { + fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName) + } +} + +func RunAlbumListFile(name string) { + if len(name) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + + activeUser := GetActiveUser() + record := getAlbumFromName(activeUser, name) + if record == nil { + return + } + + fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ + AlbumId: record.AlbumId, + }) + if er != nil { + fmt.Printf("获取相簿文件列表失败:%s\n", er) + return + } + renderTable(opLs, false, "", fileList) +} + +func RunAlbumRmFile(name string, nameList []string) { + if len(name) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + if len(nameList) == 0 { + fmt.Printf("指定文件不能为空\n") + return + } + + activeUser := GetActiveUser() + album := getAlbumFromName(activeUser, name) + if album == nil { + return + } + + fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ + AlbumId: album.AlbumId, + }) + if er != nil { + fmt.Printf("获取相簿文件列表失败:%s\n", er) + return + } + param := &aliyunpan_web.AlbumDeleteFileParam{ + AlbumId: album.AlbumId, + DriveFileList: []aliyunpan.FileBatchActionParam{}, + } + for _, file := range fileList { + if len(nameList) == 0 { + break + } + for i, name := range nameList { + if name == file.FileName { + nameList = append(nameList[:i], nameList[i+1:]...) + param.AddFileItem(file.DriveId, file.FileId) + break + } + } + } + + // 1-500 范围 + if len(param.DriveFileList) == 0 { + fmt.Printf("没有符合的文件\n") + return + } + // delete file + _, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param) + if e != nil { + fmt.Printf("删除相簿文件失败:%s\n", e) + return + } + fmt.Printf("删除相簿文件成功:%s\n", name) +} + +// RunAlbumAddFile 增加网盘文件到相簿 +func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) { + activeUser := GetActiveUser() + + if albumName == "" { + fmt.Printf("必须指定相簿名称\n") + return + } + album := getAlbumFromName(activeUser, albumName) + if album == nil { + fmt.Printf("相簿不存在\n") + return + } + + paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...) + if err != nil { + fmt.Println(err) + return + } + if len(paths) == 0 { + fmt.Printf("没有有效的文件\n") + return + } + + fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n") + param := &aliyunpan_web.AlbumAddFileParam{ + AlbumId: album.AlbumId, + DriveFileList: []aliyunpan.FileBatchActionParam{}, + } + for k := range paths { + filePath := paths[k] + fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath) + if apierr != nil { + fmt.Printf("获取文件信息失败: %s\n", filePath) + continue + } + if fileInfo.IsFile() { + // file + if isFileMatchCondition(fileInfo, filterOption) { + param.AddFileItem(fileInfo.DriveId, fileInfo.FileId) + } + } else { + // folder + activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool { + if apiError != nil { + logger.Verbosef("%s\n", apiError) + return true + } + if !fd.IsFolder() { + if isFileMatchCondition(fd, filterOption) { + param.AddFileItem(fd.DriveId, fd.FileId) + } + } + time.Sleep(2 * time.Second) + return true + }) + } + time.Sleep(2 * time.Second) + } + + if len(param.DriveFileList) == 0 { + fmt.Printf("没有符合的文件\n") + return + } + // add file + _, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param) + if e != nil { + fmt.Printf("增加相簿文件失败:%s\n", e) + return + } + fmt.Printf("增加相簿文件成功:%s\n", albumName) +} + +func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool { + if fileInfo == nil { + return false + } + if filterOption == ImageOnlyOption { + return fileInfo.Category == "image" + } else if filterOption == VideoOnlyOption { + return fileInfo.Category == "video" + } else if filterOption == ImageVideoOnlyOption { + return fileInfo.Category == "image" || fileInfo.Category == "video" + } else if filterOption == AllFileOption { + return true + } + return false +} + +func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { + if len(albumNames) == 0 { + fmt.Printf("相簿名称不能为空\n") + return + } + + activeUser := GetActiveUser() + activeUser.PanClient().WebapiPanClient().EnableCache() + activeUser.PanClient().WebapiPanClient().ClearCache() + defer activeUser.PanClient().WebapiPanClient().DisableCache() + // pan token expired checker + continueFlag := int32(0) + atomic.StoreInt32(&continueFlag, 0) + defer func() { + atomic.StoreInt32(&continueFlag, 1) + }() + go func(flag *int32) { + for atomic.LoadInt32(flag) == 0 { + time.Sleep(time.Duration(1) * time.Minute) + if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) { + logger.Verboseln("update access token for download task") + } + } + }(&continueFlag) + + if options == nil { + options = &DownloadOptions{} + } + + if options.MaxRetry < 0 { + options.MaxRetry = pandownload.DefaultDownloadMaxRetry + } + options.IsExecutedPermission = false + + // 设置下载配置 + cfg := &downloader.Config{ + Mode: transfer.RangeGenMode_BlockSize, + CacheSize: config.Config.CacheSize, + BlockSize: MaxDownloadRangeSize, + MaxRate: config.Config.MaxDownloadRate, + InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON, + ShowProgress: options.ShowProgress, + ExcludeNames: options.ExcludeNames, + } + if cfg.CacheSize == 0 { + cfg.CacheSize = int(DownloadCacheSize) + } + + // 设置下载最大并发量 + if options.Parallel < 1 { + options.Parallel = config.Config.MaxDownloadParallel + if options.Parallel == 0 { + options.Parallel = config.DefaultFileDownloadParallelNum + } + } + if options.Parallel > config.MaxFileDownloadParallelNum { + options.Parallel = config.MaxFileDownloadParallelNum + } + + // 保存文件的本地根文件夹 + originSaveRootPath := "" + if options.SaveTo != "" { + originSaveRootPath = options.SaveTo + } else { + // 使用默认的保存路径 + originSaveRootPath = GetActiveUser().GetSavePath("") + } + fi, err1 := os.Stat(originSaveRootPath) + if err1 != nil && !os.IsExist(err1) { + os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录 + } else { + if !fi.IsDir() { + fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath) + return + } + } + + fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2)) + + var ( + panClient = activeUser.PanClient() + ) + cfg.MaxParallel = options.Parallel + + var ( + executor = taskframework.TaskExecutor{ + IsFailedDeque: true, // 统计失败的列表 + } + statistic = &pandownload.DownloadStatistic{} + ) + // 配置执行器任务并发数,即同时下载文件并发数 + executor.SetParallel(cfg.MaxParallel) + + // 全局速度统计 + globalSpeedsStat := &speeds.Speeds{} + + // 处理队列 + for k := range albumNames { + record := getAlbumFromName(activeUser, albumNames[k]) + if record == nil { + continue + } + // 获取相簿下的所有文件 + fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{ + AlbumId: record.AlbumId, + }) + if er != nil { + fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k]) + continue + } + if fileList == nil || len(fileList) == 0 { + fmt.Printf("相簿里面没有文件: %s\n", albumNames[k]) + continue + } + for _, f := range fileList { + // 补全虚拟网盘路径,规则:/<相簿名称>/文件名称 + f.Path = "/" + albumNames[k] + "/" + f.FileName + + // 生成下载项 + newCfg := *cfg + unit := pandownload.DownloadTaskUnit{ + Cfg: &newCfg, // 复制一份新的cfg + PanClient: panClient, + VerbosePrinter: panCommandVerbose, + PrintFormat: downloadPrintFormat(options.Load), + ParentTaskExecutor: &executor, + DownloadStatistic: statistic, + IsPrintStatus: options.IsPrintStatus, + IsExecutedPermission: options.IsExecutedPermission, + IsOverwrite: options.IsOverwrite, + NoCheck: options.NoCheck, + FilePanPath: f.Path, + DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘) + GlobalSpeedsStat: globalSpeedsStat, + FileRecorder: nil, + } + + // TODO: 相册下载需要重构 + // 设置相簿文件信息 + //unit.SetFileInfo(pandownload.AlbumFileSource, f) + + // 设置储存的路径 + if options.SaveTo != "" { + unit.OriginSaveRootPath = options.SaveTo + unit.SavePath = filepath.Join(options.SaveTo, f.Path) + } else { + // 使用默认的保存路径 + unit.OriginSaveRootPath = GetActiveUser().GetSavePath("") + unit.SavePath = GetActiveUser().GetSavePath(f.Path) + } + info := executor.Append(&unit, options.MaxRetry) + fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path) + } + } + + // 开始计时 + statistic.StartTimer() + + // 开始执行 + executor.Execute() + + fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2)) + + // 输出失败的文件列表 + failedList := executor.FailedDeque() + if failedList.Size() != 0 { + fmt.Printf("以下文件下载失败: \n") + tb := cmdtable.NewTable(os.Stdout) + for e := failedList.Shift(); e != nil; e = failedList.Shift() { + item := e.(*taskframework.TaskInfoItem) + tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath}) + } + tb.Render() + } +} diff --git a/internal/command/download.go b/internal/command/download.go index ef5de55..83f2c7b 100644 --- a/internal/command/download.go +++ b/internal/command/download.go @@ -20,6 +20,7 @@ import ( "github.com/tickstep/aliyunpan/internal/config" "github.com/tickstep/aliyunpan/internal/file/downloader" "github.com/tickstep/aliyunpan/internal/functions/pandownload" + "github.com/tickstep/aliyunpan/internal/global" "github.com/tickstep/aliyunpan/internal/log" "github.com/tickstep/aliyunpan/internal/taskframework" "github.com/tickstep/aliyunpan/internal/utils" @@ -415,6 +416,7 @@ func RunDownload(paths []string, options *DownloadOptions) { IsExecutedPermission: options.IsExecutedPermission, IsOverwrite: options.IsOverwrite, NoCheck: options.NoCheck, + FilePanSource: global.FileSource, FilePanPath: f.Path, DriveId: options.DriveId, GlobalSpeedsStat: globalSpeedsStat, diff --git a/internal/file/downloader/downloader.go b/internal/file/downloader/downloader.go index c49ff5f..4d68ba1 100644 --- a/internal/file/downloader/downloader.go +++ b/internal/file/downloader/downloader.go @@ -20,6 +20,7 @@ import ( "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" "github.com/tickstep/aliyunpan/cmder/cmdutil" "github.com/tickstep/aliyunpan/internal/config" + "github.com/tickstep/aliyunpan/internal/global" "github.com/tickstep/aliyunpan/internal/waitgroup" "github.com/tickstep/aliyunpan/library/requester/transfer" "github.com/tickstep/library-go/cachepool" @@ -54,6 +55,7 @@ type ( monitorCancelFunc context.CancelFunc globalSpeedsStat *speeds.Speeds // 全局速度统计 + filePanSource global.FileSourceType // 要下载的网盘文件来源 fileInfo *aliyunpan.FileEntity // 下载的文件信息 driveId string loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数 @@ -97,11 +99,11 @@ func NewDownloader(writer io.WriterAt, config *Config, p *config.PanClient, sp [ return } -// SetClient 设置http客户端 -func (der *Downloader) SetFileInfo(f *aliyunpan.FileEntity) { +// SetFileInfo 设置文件信息 +func (der *Downloader) SetFileInfo(source global.FileSourceType, f *aliyunpan.FileEntity) { + der.filePanSource = source der.fileInfo = f } - func (der *Downloader) SetDriveId(driveId string) { der.driveId = driveId } @@ -389,27 +391,7 @@ func (der *Downloader) Execute() error { writeMu = &sync.Mutex{} ) - // 获取下载链接 - //var apierr *apierror.ApiError - //durl, apierr := der.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{ - // DriveId: der.driveId, - // FileId: der.fileInfo.FileId, - //}) - //time.Sleep(time.Duration(200) * time.Millisecond) - //if apierr != nil { - // logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId) - // cmdutil.Trigger(der.onCancelEvent) - // return apierr - //} - //if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) { - // logger.Verbosef("无法获取有效的下载链接: %+v\n", durl) - // cmdutil.Trigger(der.onCancelEvent) - // der.removeInstanceState() // 移除断点续传文件 - // cmdutil.Trigger(der.onFailedEvent) - // return ErrFileDownloadForbidden - //} - - // 获取各个网盘客户端的下载链接 + // 获取各个账号网盘客户端的下载链接 panClientFileUrl, err := der.getFileAllClientDownloadUrl() if err != nil || panClientFileUrl == nil { logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId) @@ -476,8 +458,19 @@ func (der *Downloader) Execute() error { return err } -// 获取对应网盘的下载链接 +// 获取对应网盘文件的下载链接 func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEntity, error) { + if der.filePanSource == global.AlbumSource { + // 相册源只支持主账号下载 + return der.getAlbumSourceDownloadUrl() + } else { + // 文件源支持多账号分流下载 + return der.getFileSourceDownloadUrl() + } +} + +// getFileSourceDownloadUrl 获取文件源的文件下载链接 +func (der *Downloader) getFileSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) { result := []*panClientDownloadUrlEntity{} // 主账号(必须存在) @@ -523,7 +516,7 @@ func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEnt // 网盘文件路径 mainFileFullPath := der.fileInfo.Path - // 铺助账号的下载链接 + // 铺助账号的下载链接,只支持文件源 if der.subPanClientList != nil { for _, spc := range der.subPanClientList { driveId := "" @@ -570,6 +563,60 @@ func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEnt return result, nil } +// getAlbumSourceDownloadUrl 获取相册源的文件下载链接 +func (der *Downloader) getAlbumSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) { + result := []*panClientDownloadUrlEntity{} + + // 相册源只能使用主账号 + // 获取下载链接 + var apierr *apierror.ApiError + durl, apierr := der.panClient.OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{ + AlbumId: der.fileInfo.AlbumId, + DriveId: der.fileInfo.DriveId, + FileId: der.fileInfo.FileId, + }) + time.Sleep(time.Duration(200) * time.Millisecond) + if apierr != nil { + logger.Verbosef("ERROR: get album file download url error: %s\n", der.fileInfo.FileId) + cmdutil.Trigger(der.onCancelEvent) + return nil, apierr + } + realUrl := "" + if durl.StreamsUrl != nil { // 实况图片 + if strings.ToLower(der.fileInfo.FileExtension) == "mov" { + // 视频文件 + if durl.StreamsUrl.Mov != "" { + realUrl = durl.StreamsUrl.Mov + } + } else { + // 照片文件 + if durl.StreamsUrl.Heic != "" { + realUrl = durl.StreamsUrl.Heic + } else if durl.StreamsUrl.Jpeg != "" { + realUrl = durl.StreamsUrl.Jpeg + } + } + } else if durl.Url != "" { // 普通图片 + realUrl = durl.Url + } + if realUrl == "" { + logger.Verbosef("无法获取有效的相册文件下载链接: %+v\n", durl) + cmdutil.Trigger(der.onCancelEvent) + der.removeInstanceState() // 移除断点续传文件 + cmdutil.Trigger(der.onFailedEvent) + return nil, ErrFileDownloadForbidden + } + // 返回下载链接 + result = append(result, &panClientDownloadUrlEntity{ + PanClient: der.panClient, + FileInfo: der.fileInfo, + DriveId: der.driveId, + FileId: der.fileInfo.FileId, + FileUrl: realUrl, + }) + return result, nil +} + // downloadStatusEvent 执行状态处理事件 func (der *Downloader) downloadStatusEvent() { if der.onDownloadStatusEvent == nil { diff --git a/internal/functions/pandownload/download_task_unit.go b/internal/functions/pandownload/download_task_unit.go index 7c7d015..8707745 100644 --- a/internal/functions/pandownload/download_task_unit.go +++ b/internal/functions/pandownload/download_task_unit.go @@ -21,6 +21,7 @@ import ( "github.com/tickstep/aliyunpan/internal/config" "github.com/tickstep/aliyunpan/internal/file/downloader" "github.com/tickstep/aliyunpan/internal/functions" + "github.com/tickstep/aliyunpan/internal/global" "github.com/tickstep/aliyunpan/internal/localfile" "github.com/tickstep/aliyunpan/internal/log" "github.com/tickstep/aliyunpan/internal/plugins" @@ -40,8 +41,6 @@ import ( ) type ( - FileSourceType string - // DownloadTaskUnit 下载的任务单元 DownloadTaskUnit struct { taskInfo *taskframework.TaskInfo // 任务信息 @@ -62,10 +61,11 @@ type ( IsOverwrite bool // 是否覆盖已存在的文件 NoCheck bool // 不校验文件 - FilePanPath string // 要下载的网盘文件路径 - SavePath string // 文件保存在本地的路径 - OriginSaveRootPath string // 文件保存在本地的根目录路径 - DriveId string + FilePanSource global.FileSourceType // 要下载的网盘文件来源 + FilePanPath string // 要下载的网盘文件路径 + SavePath string // 文件保存在本地的路径 + OriginSaveRootPath string // 文件保存在本地的根目录路径 + DriveId string // 网盘ID fileInfo *aliyunpan.FileEntity // 文件或目录详情 @@ -89,15 +89,14 @@ const ( StrDownloadChecksumFailed = "检测文件有效性失败" // DefaultDownloadMaxRetry 默认下载失败最大重试次数 DefaultDownloadMaxRetry = 3 - - // BackupFileSource 备份盘文件 - BackupFileSource FileSourceType = "backup" - // ResourceFileSource 资源库文件 - ResourceFileSource FileSourceType = "resource" - // AlbumFileSource 相册文件 - AlbumFileSource FileSourceType = "album" ) +// SetFileInfo 设置文件信息 +func (dtu *DownloadTaskUnit) SetFileInfo(source global.FileSourceType, f *aliyunpan.FileEntity) { + dtu.FilePanSource = source + dtu.fileInfo = f +} + func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) { dtu.taskInfo = info } @@ -172,7 +171,7 @@ func (dtu *DownloadTaskUnit) download() (err error) { defer file.Close() der := downloader.NewDownloader(writer, dtu.Cfg, dtu.PanClient, dtu.SubPanClientList, dtu.GlobalSpeedsStat) - der.SetFileInfo(dtu.fileInfo) + der.SetFileInfo(dtu.FilePanSource, dtu.fileInfo) der.SetDriveId(dtu.DriveId) der.SetStatusCodeBodyCheckFunc(func(respBody io.Reader) error { // 解析错误 @@ -568,6 +567,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) { newCfg := *dtu.Cfg subUnit.Cfg = &newCfg subUnit.fileInfo = fileList[k] // 保存文件信息 + subUnit.FilePanSource = dtu.FilePanSource subUnit.FilePanPath = fileList[k].Path subUnit.SavePath = filepath.Join(dtu.OriginSaveRootPath, fileList[k].Path) // 保存位置 diff --git a/internal/global/pan_file.go b/internal/global/pan_file.go new file mode 100644 index 0000000..fc488d8 --- /dev/null +++ b/internal/global/pan_file.go @@ -0,0 +1,12 @@ +package global + +type ( + FileSourceType string +) + +const ( + // FileSource 文件,包括:资源盘文件、备份盘文件 + FileSource FileSourceType = "file" + // AlbumSource 相册 + AlbumSource FileSourceType = "album" +) diff --git a/main.go b/main.go index cc4cb16..40885a1 100644 --- a/main.go +++ b/main.go @@ -478,6 +478,9 @@ func main() { // 分享文件/目录 share command.CmdShare(), + // 相簿 + command.CmdAlbum(), + // 显示命令历史 { Name: "history", @@ -686,9 +689,6 @@ func main() { // Category: "debug", // Before: command.ReloadConfigFunc, // Action: func(c *cli.Context) error { - // os.Setenv(config.EnvVerbose, "1") - // logger.IsVerbose = true - // fmt.Println("显示调试日志", logger.IsVerbose) // return nil // }, // Flags: []cli.Flag{