├── README.md └── epub-comic-repacker.go /README.md: -------------------------------------------------------------------------------- 1 | # epub-comic-repacker 2 | extract the image files from epub comic books, and repack them into zip files, fits for any epub files, expecially downloaded from vol.me or mox.moe 3 | 4 | ### build 5 | after "go build", the porgamme will run in CMD like mode without gui, but you can add an icon to make it look better, just follow this up: 6 | 7 | #### in Windows 8 | Create file epub-comic-repacker.manifest 9 | ``` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PerMonitorV2, PerMonitor 21 | True 22 | 23 | 24 | 25 | ``` 26 | 27 | prepare an icon file named vol.ico 28 | ``` 29 | rsrc -manifest epub-comic-repacker.manifest -ico vol.ico -o rsrc.syso 30 | go build 31 | ``` 32 | 33 | #### in MacOS 34 | prepare a PNG file in 1024p, and use the link file downside to build automatically 35 | 36 | https://gist.github.com/mholt/11008646c95d787c30806d3f24b2c844 37 | -------------------------------------------------------------------------------- /epub-comic-repacker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "golang.org/x/net/html" 18 | ) 19 | 20 | func shinkName(filename string) string { 21 | fbase := filepath.Base(filename) 22 | reg, err := regexp.Compile(`(.*moe\])(.*)(\.kepub)(\.epub)$`) 23 | if err != nil { 24 | panic("regexp works with error") 25 | } 26 | shinkname := reg.ReplaceAllString(fbase, "$2$4") 27 | return shinkname 28 | } 29 | 30 | func getMainName(filename string) string { 31 | fbase := filepath.Base(filename) 32 | fext := filepath.Ext(filename) 33 | fmain := strings.TrimSuffix(fbase, fext) 34 | return fmain 35 | } 36 | 37 | func findAttrValue(r io.Reader, attrname string) (value string) { 38 | tokenizer := html.NewTokenizer(r) 39 | for tokenizer.Token().Data != "html" { 40 | tt := tokenizer.Next() 41 | if tt == html.ErrorToken { 42 | if tokenizer.Err() == io.EOF { 43 | return 44 | } 45 | fmt.Printf("Error: %v", tokenizer.Err()) 46 | return 47 | } 48 | tagName, _ := tokenizer.TagName() 49 | //to find the first value of tag named "img" 50 | if string(tagName) == "img" { 51 | attrKey, attrValue, _ := tokenizer.TagAttr() 52 | if string(attrKey) == attrname { 53 | return string(attrValue) 54 | } 55 | } 56 | } 57 | return 58 | } 59 | 60 | func unZipFiles(sourcefile string, cachefolder string) ([]string, error) { 61 | var extractfiles, extractimgs []string 62 | var imgname, imgpath []string 63 | r, err := zip.OpenReader(sourcefile) 64 | if err != nil { 65 | return extractfiles, err 66 | } 67 | defer r.Close() 68 | for _, f := range r.File { 69 | // //extract source zipfile with full address 70 | // desfpath := filepath.Join(cachefolder, getMainName(sourcefile), f.Name) 71 | coversavepath := filepath.Join(cachefolder, getMainName(sourcefile), filepath.Base(f.Name)) 72 | extractfiles = append(extractfiles, coversavepath) 73 | // //create directory 74 | // if f.FileInfo().IsDir() { 75 | // os.MkdirAll(desfpath, os.ModePerm) 76 | // continue 77 | // } 78 | if err = os.MkdirAll(filepath.Dir(coversavepath), os.ModePerm); err != nil { 79 | return extractfiles, err 80 | } 81 | 82 | //to get cover.jpg 83 | reg, err := regexp.Compile(`cover\.(jpg|png)$`) 84 | if err != nil { 85 | panic("regexp works with error") 86 | } 87 | if reg.MatchString(f.FileInfo().Name()) { 88 | outfile, err := os.OpenFile(coversavepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 89 | if err != nil { 90 | return extractfiles, err 91 | } 92 | //open file in source zipfile 93 | rc, err := f.Open() 94 | if err != nil { 95 | return extractfiles, err 96 | } 97 | _, err = io.Copy(outfile, rc) 98 | //not use defer to close the file 99 | outfile.Close() 100 | rc.Close() 101 | if err != nil { 102 | return extractfiles, err 103 | } 104 | } 105 | 106 | //to get the "src" info pased from html and prepare the image names 107 | reg, err = regexp.Compile(`\d\.(html)$`) 108 | if err != nil { 109 | log.Fatal("regexp works with error") 110 | } 111 | if reg.MatchString(f.FileInfo().Name()) { 112 | in := getMainName(f.FileInfo().Name()) + ".jpg" 113 | imgname = append(imgname, in) 114 | 115 | rc, err := f.Open() 116 | if err != nil { 117 | return extractfiles, err 118 | } 119 | //to get the tag named "src" 120 | tagvalue := findAttrValue(rc, "src") 121 | imgpath = append(imgpath, tagvalue) 122 | rc.Close() 123 | if err != nil { 124 | return extractfiles, err 125 | } 126 | } 127 | } 128 | 129 | //second loop to change the parsed image name to the name of html 130 | for _, f2 := range r.File { 131 | for i := 0; i < len(imgname); i++ { 132 | imgsavepath := filepath.Join(cachefolder, getMainName(sourcefile), imgname[i]) 133 | extractimgs = append(extractimgs, imgsavepath) 134 | 135 | if f2.FileInfo().Name() == filepath.Base(imgpath[i]) { 136 | outfile, err := os.OpenFile(imgsavepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f2.Mode()) 137 | if err != nil { 138 | return extractimgs, err 139 | } 140 | 141 | rc, err := f2.Open() 142 | if err != nil { 143 | return extractimgs, err 144 | } 145 | _, err = io.Copy(outfile, rc) 146 | 147 | outfile.Close() 148 | rc.Close() 149 | fmt.Println("Extract: " + imgsavepath) 150 | if err != nil { 151 | return extractimgs, err 152 | } 153 | } 154 | } 155 | } 156 | return extractfiles, nil 157 | } 158 | 159 | func getFilelist(folder string) ([]string, error) { 160 | var result []string 161 | filepath.Walk(folder, func(path string, fi os.FileInfo, err error) error { 162 | if err != nil { 163 | log.Println(err.Error()) 164 | return err 165 | } 166 | if !fi.IsDir() { 167 | //if want to ignore the directory, return filepath.SkipDir 168 | //return filepath.SkipDir 169 | result = append(result, path) 170 | } 171 | return nil 172 | }) 173 | return result, nil 174 | } 175 | 176 | func zipFiles(desfile string, srcfiles []string, oldform, newform string) error { 177 | newZipFile, err := os.Create(desfile) 178 | if err != nil { 179 | return err 180 | } 181 | defer newZipFile.Close() 182 | zipWriter := zip.NewWriter(newZipFile) 183 | defer zipWriter.Close() 184 | 185 | //add files to zip 186 | for _, srcfile := range srcfiles { 187 | zipfile, err := os.Open(srcfile) 188 | if err != nil { 189 | return err 190 | } 191 | defer zipfile.Close() 192 | //get the info of file 193 | info, err := zipfile.Stat() 194 | if err != nil { 195 | return err 196 | } 197 | header, err := zip.FileInfoHeader(info) 198 | if err != nil { 199 | return err 200 | } 201 | //modify FileInforHeader() so to change any address we want 202 | header.Name = strings.Replace(srcfile, oldform, newform, -1) 203 | // optimize zip 204 | // more to reference http://golang.org/pkg/archive/zip/#pkg-constants 205 | header.Method = zip.Deflate 206 | 207 | writer, err := zipWriter.CreateHeader(header) 208 | if err != nil { 209 | return err 210 | } 211 | if _, err = io.Copy(writer, zipfile); err != nil { 212 | return err 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | func setupCloseHandler() { 219 | c := make(chan os.Signal) 220 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 221 | go func() { 222 | <-c 223 | fmt.Println("\r- Ctrl+C pressed in Terminal") 224 | os.Exit(0) 225 | }() 226 | } 227 | 228 | func main() { 229 | //Setup Ctrl+C handler 230 | setupCloseHandler() 231 | //create temporary directory "cache" 232 | const cachefolder string = "cache" 233 | var shunkzipfiles []string 234 | err := os.Mkdir(cachefolder, os.ModePerm) 235 | if os.IsExist(err) { 236 | log.Println("cachefolder already exists, ignore creating") 237 | } 238 | //create save directory "ouput" 239 | const outputfolder string = "output" 240 | err = os.Mkdir(outputfolder, os.ModePerm) 241 | if os.IsExist(err) { 242 | log.Println("outputfolder already exists, ignore creating") 243 | } 244 | 245 | //shink the name of .epub and change to .zip 246 | epubfiles, err := filepath.Glob("*.epub") 247 | if err != nil { 248 | log.Fatal("pattern error") 249 | } 250 | if epubfiles == nil { 251 | log.Fatal("no epub files found") 252 | } 253 | for _, efile := range epubfiles { 254 | zfile := getMainName(shinkName(efile)) + ".zip" 255 | os.Rename(efile, zfile) 256 | shunkzipfiles = append(shunkzipfiles, zfile) 257 | } 258 | 259 | //unzip .zip files 260 | for _, szfile := range shunkzipfiles { 261 | //ignore the file list of unzip file 262 | _, err := unZipFiles(szfile, cachefolder) 263 | if err != nil { 264 | log.Fatal(err) 265 | } 266 | } 267 | 268 | //get the directory name under cache folder 269 | folders, _ := ioutil.ReadDir(cachefolder) 270 | var foldenames []string 271 | for _, fo := range folders { 272 | foldenames = append(foldenames, fo.Name()) 273 | } 274 | 275 | for _, foldename := range foldenames { 276 | storename := outputfolder + string(os.PathSeparator) + foldename + ".zip" 277 | sourcefolder := cachefolder + string(os.PathSeparator) + foldename 278 | filelist, err := getFilelist(sourcefolder) 279 | if err != nil { 280 | log.Fatalln("getAfllFiles gos wrong") 281 | } 282 | err = zipFiles(storename, filelist, sourcefolder+string(os.PathSeparator), "") 283 | if err != nil { 284 | log.Fatalln("zipFiles gos worng") 285 | } 286 | fmt.Printf("Zipfile: %s\n", storename) 287 | } 288 | 289 | //delete cachefolder 290 | err = os.RemoveAll(cachefolder) 291 | if err != nil { 292 | log.Println("delete cachefolder error") 293 | } 294 | //chagnge files extendtion back to .epub 295 | for _, szfiles := range shunkzipfiles { 296 | err := os.Rename(szfiles, getMainName(szfiles)+".epub") 297 | if err != nil { 298 | log.Println("rename shunkzipfiles error") 299 | } 300 | } 301 | fmt.Println("All finished, quit in 3s later") 302 | time.Sleep(3 * time.Second) 303 | } 304 | --------------------------------------------------------------------------------