1712 字
9 分钟
AI魔法师 吟唱一个小工具:bookimporter
2023-02-24

最近我使用魔法吟唱声明了一个Go语言的工具:BookImporter。这个工具可以帮助你快速导入电子书到你的电子书管理软件中,比如Calibre。它支持多种电子书格式,包括EPUB、MOBI、PDF等。

起因#

我最近下载了一些电子书,过滤出epub文件,大概有7w多本。通常我会对它们进行重命名为 ebook-a-$n格式,以方便导入后对损坏的书籍清理。 重命名我有两种方式,

  1. 使用rename命令批量处理,例如 rename 's/.*\.epub$/ebook-$N.epub/' /book-import/*.epub
  2. 使用macOS访达的批量进行处理,一次最多1w本。 通常我会使用方式1,因为文件太多了,一行命令就可以搞定。但是rename并不ok,也提示我文件太多了。无奈我只好手动处理了。

写个工具把整个导入流程搞一下也是可以的,先试试AI吟唱写代码吧。

代码生成#

目前我加了rename的功能,用来批量重命名epub文件。

该工具具有以下参数:

--debug                启用调试信息
--do-try               仅输出将要执行的操作,而不实际重命名文件
-f, --format stringArray   要匹配的文件格式(例如 'txt')(默认为 [*])
-h, --help                 显示 rename 命令的帮助信息
-o, --output string        移动文件的输出目录
-r, --recursive            递归搜索文件
--start-num int        序列的起始编号(默认为 1)
-t, --template string      新文件名的模板(例如 'file-$n')(默认为 "file-$n")

吟唱内容#

写golang的小工具,使用cobra定义命令的输出,定义子命令rename,能够按格式匹配文件,然后进行批量重命名。可选参数有—debug开启debug信息输出,—do-try表示仅打印信息不重命名和移动文件,-f文件格式,-r递归解析,-m移动文件,-o输出文件夹,-t模板用于配置新文件名,-i表示模板中n的起始数字。f文件格式可以写为:全匹配或者epub:匹配epub文件epubazw3:匹配epubazw3文件。t模板模板可选参数n的起始数字。-f文件格式可以写为 * :全匹配或者 epub :匹配epub文件 epub|azw3 :匹配epub和azw3文件。-t模板 模板可选参数n 是文件序号,示例模板 ebook-$n 表示会将文件重命名为ebook-1、ebook-2这样的文件名,保留原文件格式。这些参数都会绑定到名为RenameConfig的结构上。

在我的吟唱下,ChatGPT生成了以下代码:

package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

type RenameConfig struct {
	Debug      bool
	DoTry      bool
	Format     string
	Recursive  bool
	Move       bool
	OutputDir  string
	Template   string
	StartIndex int
}

func main() {
	rootCmd := &cobra.Command{
		Use:   "bookImporter",
		Short: "Import books into your library",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("bookImporter v0.1")
		},
	}

	renameCmd := &cobra.Command{
		Use:   "rename",
		Short: "Rename or move files according to a template",
		Long: `Rename or move files according to a template.

The rename command will rename or move files according to a template that you
specify. The template can include the file extension, so if you want to keep
the original extension, you can include it in the template. You can also use
a sequence number in the template to number the files sequentially.`,
		Run: func(cmd *cobra.Command, args []string) {
			config := RenameConfig{
				Debug:      cmd.Flag("debug").Value.String() == "true",
				DoTry:      cmd.Flag("do-try").Value.String() == "true",
				Format:     cmd.Flag("format").Value.String(),
				Recursive:  cmd.Flag("recursive").Value.String() == "true",
				Move:       cmd.Flag("move").Value.String() == "true",
				OutputDir:  cmd.Flag("output").Value.String(),
				Template:   cmd.Flag("template").Value.String(),
				StartIndex: parseIntFlag(cmd, "start-num"),
			}

			if config.Debug {
				fmt.Println("Debugging information:")
				fmt.Printf("  - Format: %s\n", config.Format)
				fmt.Printf("  - Recursive: %v\n", config.Recursive)
				fmt.Printf("  - Move: %v\n", config.Move)
				fmt.Printf("  - OutputDir: %s\n", config.OutputDir)
				fmt.Printf("  - Template: %s\n", config.Template)
				fmt.Printf("  - StartIndex: %d\n", config.StartIndex)
			}

			files, err := findFiles(config.Format, config.Recursive)
			if err != nil {
				fmt.Printf("Error: %s\n", err)
				os.Exit(1)
			}

			if len(files) == 0 {
				fmt.Println("No files found.")
				return
			}

			var (
				renamedFiles []string
				movedFiles   []string
			)

			for i, file := range files {
				newName := buildNewName(config.Template, config.StartIndex+i, file)

				if config.Move {
					outputDir := config.OutputDir
					if outputDir == "" {
						outputDir = filepath.Dir(file)
					}

					outputPath := filepath.Join(outputDir, newName)

					if !config.DoTry {
						err = os.Rename(file, outputPath)
						if err != nil {
							fmt.Printf("Error: %s\n", err)
							os.Exit(1)
						}
					}

					movedFiles = append(movedFiles, file+" -> "+outputPath)
				} else {
					outputPath := filepath.Join(filepath.Dir(file), newName)

					if !config.DoTry {
						err = os.Rename(file, outputPath)
						if err != nil {
							fmt.Printf("Error: %s\n", err)
							os.Exit(1)
						}
					}
					renamedFiles = append(renamedFiles, file+" -> "+outputPath)
				}

			}

			fmt.Printf("%d files found.\n", len(files)),
			if config.Move {
				fmt.Printf("%d files moved:\n", len(movedFiles))
				for _, file := range movedFiles {
					fmt.Printf("  - %s\n", file)
				}
			} else {
				fmt.Printf("%d files renamed:\n", len(renamedFiles))
				for _, file := range renamedFiles {
					fmt.Printf("  - %s\n", file)
				}
			}
		},
	}

	renameCmd.Flags().Bool("debug", false, "Enable debugging information")
	renameCmd.Flags().Bool("do-try", false, "Only print out actions that would be performed")
	renameCmd.Flags().String("format", "*", "File format to match (e.g. '*.txt')")
	renameCmd.Flags().Bool("recursive", false, "Recursively search for files")
	renameCmd.Flags().Bool("move", false, "Move files instead of renaming them")
	renameCmd.Flags().String("output", "", "Output directory for moved files")
	renameCmd.Flags().String("template", "file-$n", "Template for new filename")
	renameCmd.Flags().Int("start-num", 1, "Starting number for sequence")

	rootCmd.AddCommand(renameCmd)

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

}

func findFiles(format string, recursive bool) ([]string, error) {
	var files []string

	matches, err := filepath.Glob(format)
	if err != nil {
		return nil, err
	}

	for _, match := range matches {
		info, err := os.Stat(match)
		if err != nil {
			return nil, err
		}

		if !info.IsDir() {
			files = append(files, match)
		} else if recursive {
			subFiles, err := findFiles(filepath.Join(match, format), true)
			if err != nil {
				return nil, err
			}

			files = append(files, subFiles...)
		}
	}

	return files, nil

}

func buildNewName(template string, index int, file string) string {
	ext := filepath.Ext(file)
	newName := strings.Replace(template, "$n", strconv.Itoa(index), -1)
	newName = strings.Replace(newName, "$e", ext, -1)

	if ext != "" && !strings.Contains(newName, "$e") {
		newName += ext
	}

	return newName
}

func parseIntFlag(cmd *cobra.Command, name string) int {
	val, _ := cmd.Flags().GetInt(name)
	return val
}

我拿过来代码,大致改了改,就可以使用了。

代码放在了github上

jianyun8023
/
bookimporter
Waiting for api.github.com...
00K
0K
0K
Waiting...

文档#

接下来我使用了Notion AI生成了Readme

魔法吟唱#

写一下bookimporter的介绍,这是我自己向calibre书库导入书籍的工作流。通常我会过滤出epub文件,然后进行批量重命名,接着对epub中的书籍标题进行清理,去除一堆冗长的名称。接下来会调用calibredb进行书籍的导入。

生成的描述#

BookImporter是一款功能强大的工具,它为用户提供了一种快速、方便的方式来导入电子书到Calibre书库中。作为一名电子书爱好者,你可能会在各种平台上购买或下载电子书。但是,当你的电子书数量逐渐增多时,你可能会发现管理它们变得越来越困难。这时,BookImporter就能派上用场了。

使用BookImporter,你可以轻松地将你的电子书导入到Calibre书库中,并对它们进行管理和整理。首先,你可以使用该工具过滤出所需的epub文件,然后进行批量重命名,以方便后续的管理。接着,你可以对epub中的书籍标题进行清理,去除冗长的名称,让书名更加简洁、明了。最后,你可以使用Calibredb对书籍进行导入,以便快速管理和阅读自己的电子书。

除了上述功能外,BookImporter还具有以下优点:

  • 高效性:该工具可以快速导入大量的电子书,让你的阅读更加高效。
  • 多平台支持:BookImporter可以在多个平台上运行,包括Windows、Mac和Linux等。
  • 自定义选项:用户可以根据自己的需求,自定义BookImporter的选项,以便更好地适应自己的工作流程。

总之,如果你是一名电子书爱好者,需要对自己的电子书进行管理和整理,那么BookImporter会是一个不错的选择。它能够帮助你快速导入和管理电子书,让你的阅读更加便捷和愉快。

结尾#

AI魔法师 吟唱一个小工具:bookimporter
https://www.jianyun.run/posts/ai-creates-bookimporter/
作者
唐长老日志
发布于
2023-02-24
许可协议
CC BY-NC-SA 4.0