Skip to content

Instantly share code, notes, and snippets.

@phpgao
Last active September 20, 2023 11:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phpgao/df81898294c04207790c238c7422ab8c to your computer and use it in GitHub Desktop.
Save phpgao/df81898294c04207790c238c7422ab8c to your computer and use it in GitHub Desktop.
mange media
package main
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/tiff"
"github.com/urfave/cli/v2"
)
const layout = "2006:01:02 15:04:05"
var picTypes = map[string]bool{
"jpg": true,
"jpeg": true,
"png": true,
"gif": true,
"bmp": true,
}
var AudioTypes = map[string]bool{
"mp3": true,
"flac": false,
"wav": false,
}
var videoTypes = map[string]bool{
"mp4": true,
"mov": true,
"avi": true,
"wmv": true,
"mkv": true,
"rm": true,
"f4v": true,
"flv": true,
"swf": true,
}
var phoneMap = map[string]string{
"2304FPN6DC": "Xiaomi13Ultra",
"22021211RC": "RedmiK40S",
}
// time regex to time layout
var regexTime = map[string]string{
`\d{8}_\d{6}`: "20060102_150405",
`\d{4}-\d{2}-\d{2} \d{2}\.\d{2}\.\d{2}`: "2006-01-02 15.04.05",
}
type Config struct {
Source string
Destination string
Dry bool
Rename bool
NoSkip bool
OverWrite bool
Mode string
}
var c = Config{}
func main() {
app := &cli.App{
Name: "media tool",
Usage: "你懂的",
Version: "v0.0.1",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "dry",
Destination: &c.Dry,
Usage: "dry run",
},
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Destination: &c.Source,
Usage: "source directory",
Required: true,
},
&cli.StringFlag{
Name: "dest",
Aliases: []string{"d"},
Destination: &c.Destination,
Usage: "destination directory",
Required: true,
},
&cli.StringFlag{
Name: "mode",
Aliases: []string{"mo"},
Destination: &c.Mode,
Usage: "copy or move?",
Required: true,
},
&cli.BoolFlag{
Name: "no-skip",
Destination: &c.NoSkip,
Usage: "no skip if file exists",
},
&cli.BoolFlag{
Name: "overwrite",
Aliases: []string{"o"},
Destination: &c.OverWrite,
Usage: "overwrite if file exists",
},
},
Action: func(cCtx *cli.Context) error {
imageFileList, videoFileList, audioFileList := walk(c.Source)
todoMap := make(map[string]string)
for _, file := range imageFileList {
newPath, err := processImage(file)
if err != nil {
log.Printf("error processing %s: %v\n", file, err)
continue
}
todoMap[file] = filepath.Join(c.Destination, newPath)
}
for _, file := range videoFileList {
fmt.Println(file)
}
for _, file := range audioFileList {
fmt.Println(file)
}
// spew.Dump(todoMap)
handleFile(todoMap)
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func handleFile(m map[string]string) (err error) {
for s, d := range m {
d, err = createDestinationFile(d)
if err != nil {
log.Println(err)
continue
}
switch c.Mode {
case "copy":
log.Printf("%s is being copied to %s", s, d)
err = copyFile(s, d)
if err != nil {
return
}
case "move":
log.Printf("%s is being moved to %s", s, d)
err = moveFile(s, d)
if err != nil {
return
}
}
}
return nil
}
func createDestinationFile(dst string) (string, error) {
if fileExists(dst) {
if !c.NoSkip {
return "", errors.New("skip file " + dst)
}
if c.OverWrite {
return dst, nil
} else {
dst = generateNewFileName(dst)
return dst, nil
}
}
parentDir := filepath.Dir(dst)
if err := createParentDir(parentDir); err != nil {
return "", err
}
return dst, nil
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func createParentDir(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
}
return nil
}
func generateNewFileName(filename string) string {
ext := filepath.Ext(filename)
name := strings.TrimSuffix(filename, ext)
return name + "_new_" + time.Now().Format("20060102150405") + ext
}
func processImage(file string) (newPath string, err error) {
// Decode the EXIF data from the file
newPath, err = readExif(file)
if err != nil {
log.Printf("error processing exif %s: %v\n", file, err)
err = nil
} else {
return
}
//try file name regex second
newPath = matchWxExport(file)
if newPath == "" {
log.Printf("error processing matchWxExport %s: %v\n", file, err)
} else {
return
}
newPath = matchRegex(file)
if newPath == "" {
log.Printf("error processing regexFile %s: %v\n", file, err)
} else {
return
}
//try fstat finally
return
}
// func regexWalk(filename) time.Time {
// }
func readExif(file string) (string, error) {
fileHandle, err := os.Open(file)
if err != nil {
return "", err
}
x, err := exif.Decode(fileHandle)
if err != nil {
return "", err
}
var p string
modelInfo, err := x.Get("Model")
if err != nil {
log.Printf("can't get model info from %s", file)
} else {
model := getTagString(modelInfo)
p = phoneMap[model]
}
timeInfo, err := x.Get("DateTimeOriginal")
if err != nil {
return "", err
}
tm, _ := time.Parse(layout, getTagString(timeInfo))
year := tm.Format("2006")
month := tm.Format("01")
fileBase := filepath.Base(file)
return filepath.Join(p, year, month, fileBase), nil
}
//convert string(2023:01:24 16:34:30) to timestamp
// getModel returns the model of a given tag.
//
// It takes a pointer to a `tiff.Tag` as its parameter and returns a string.
func getTagString(tag *tiff.Tag) string {
return strings.Trim(tag.String(), "\"")
}
func matchWxExport(filename string) (newPath string) {
pattern := `mmexport(1\d{9})`
regex, _ := regexp.Compile(pattern)
submatches := regex.FindStringSubmatch(filename)
if len(submatches) > 0 {
ts := submatches[1]
i, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
panic(err)
}
tm := time.Unix(i, 0)
year := tm.Format("2006")
month := tm.Format("01")
fileBase := filepath.Base(filename)
newPath = filepath.Join(year, month, fileBase)
}
return
}
func matchRegex(file string) string {
for r, layout := range regexTime {
regex, _ := regexp.Compile(r)
submatches := regex.FindStringSubmatch(file)
if len(submatches) > 0 {
m := submatches[0]
tm, _ := time.Parse(layout, m)
year := tm.Format("2006")
month := tm.Format("01")
fileBase := filepath.Base(file)
return filepath.Join(year, month, fileBase)
}
}
return ""
}
// walk traverses the directory and collects image, video, and audio files.
//
// It takes a directory path as input and returns three slices of strings:
// imageFiles, videoFiles, and audioFiles.
func walk(dir string) (imageFiles []string, videoFiles []string, audioFiles []string) {
filepath.WalkDir(dir, func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if !file.IsDir() {
ext := getFileExtension(file.Name())
if flag := picTypes[ext]; flag {
imageFiles = append(imageFiles, path)
}
if flag := videoTypes[ext]; flag {
videoFiles = append(videoFiles, path)
}
if flag := AudioTypes[ext]; flag {
audioFiles = append(audioFiles, path)
}
}
return nil
})
return
}
// getFileExtension returns the file extension from the given path.
func getFileExtension(path string) string {
return strings.Trim(filepath.Ext(path), ".")
}
func moveFile(src, dst string) error {
return os.Rename(src, dst)
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
log.Println("Error opening source file:", err)
return err
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
log.Println("Error creating destination file:", err)
return err
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
if err != nil {
log.Println("Error copying file:", err)
return err
}
err = destinationFile.Sync()
if err != nil {
log.Println("Error syncing destination file:", err)
return err
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment