diff options
| -rw-r--r-- | ass.go | 436 | ||||
| -rw-r--r-- | go.mod | 14 | ||||
| -rw-r--r-- | go.sum | 0 | ||||
| -rw-r--r-- | main.go | 283 | ||||
| -rw-r--r-- | mkv.go | 295 | ||||
| -rw-r--r-- | title.go | 9 | ||||
| -rw-r--r-- | title_windows.go | 22 | ||||
| -rw-r--r-- | utils.go | 68 |
8 files changed, 870 insertions, 257 deletions
@@ -0,0 +1,436 @@ +package main + +import ( + "encoding/binary" + "fmt" + "github.com/antchfx/xmlquery" + "github.com/asticode/go-astisub" + "io" + "log" + "os" + "path" + "regexp" + "strconv" + "strings" + "sync" +) + +const ( + ttx = "ttx" + pyftsubset = "pyftsubset" +) + +type fontInfo struct { + file string + str string + index string + oldName string + newName string + ttx string + sFont string +} + +type ass struct { + files []string + _fonts string + output string + m map[string]*fontInfo + fonts []string + sFonts []string + subtitles map[string]string +} + +func (self *ass) parse() bool { + ec := 0 + self.subtitles = make(map[string]string) + for _, file := range self.files { + f, err := openFile(file, true, false) + if err != nil { + ec++ + } else { + data, _ := io.ReadAll(f) + str := string(data) + if err == nil { + self.subtitles[file] = str + } else { + ec++ + } + } + if ec > 0 { + log.Printf(`Failed to read the ass file: "%s"`, file) + } + } + if ec == 0 { + reg, _ := regexp.Compile(`\{?\\fn@?([^\\]+)[\\\}]`) + m := make(map[string]map[rune]bool) + for k, v := range self.subtitles { + subtitle, err := astisub.ReadFromSSA(strings.NewReader(v)) + if err != nil { + ec++ + log.Printf(`Failed to read the ass file: "%s"`, k) + continue + } + for _, item := range subtitle.Items { + for _, _item := range item.Lines { + for _, __item := range _item.Items { + name := item.Style.InlineStyle.SSAFontName + if __item.InlineStyle != nil { + arr := reg.FindStringSubmatch(__item.InlineStyle.SSAEffect) + if len(arr) > 1 { + name = arr[1] + } + } + if m[name] == nil { + m[name] = make(map[rune]bool) + } + str := __item.Text + for _, char := range str { + m[name][char] = true + } + } + } + } + } + self.m = make(map[string]*fontInfo) + reg, _ = regexp.Compile("[A-Za-z0-9]]") + for k, v := range m { + str := "" + for _k, _ := range v { + str += string(_k) + } + str = strings.TrimSpace(str) + str = reg.ReplaceAllString(str, "") + str += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + reg, _ = regexp.Compile("[1234567890]") + if reg.MatchString(str) { + str = reg.ReplaceAllString(str, "") + str += "1234567890" + } + if str != "" { + self.m[k] = new(fontInfo) + self.m[k].str = str + self.m[k].oldName = k + } + } + } + if len(self.m) == 0 { + log.Printf(`Not Found item in the ass file(s): "%d"`, len(self.files)) + } + return ec == 0 +} + +func (self *ass) getTTCCount(file string) int { + f, err := openFile(file, true, false) + if err == nil { + defer func() { _ = f.Close() }() + data := make([]byte, 4) + if n, err := f.ReadAt(data, 8); err == nil && n == 4 { + return int(binary.BigEndian.Uint32(data)) + } + } + return 0 +} + +func (self *ass) dumpFont(file string, full bool) bool { + ok := false + count := 1 + _, n, _, _ := splitPath(file) + if strings.HasSuffix(file, ".ttc") && !full { + count = self.getTTCCount(file) + if count < 1 { + log.Printf(`Failed to get the ttc font count: "%s".`, n) + return ok + } + } + for i := 0; i < count; i++ { + fn := fmt.Sprintf("%s_%d.ttx", file, i) + args := make([]string, 0) + args = append(args, "-q") + args = append(args, "-f") + args = append(args, "-y", strconv.Itoa(i)) + args = append(args, "-o", fn) + if !full { + args = append(args, "-t", "name") + } + args = append(args, file) + if p, err := newProcess(nil, nil, nil, "", ttx, args...); err == nil { + s, err := p.Wait() + ok = err == nil && s.ExitCode() == 0 + } + if !ok { + log.Printf(`Failed to dump font(%t): "%s"[%d].`, full, n, i) + } + } + return ok +} + +func (self *ass) dumpFonts(files []string, full bool) bool { + ok := 0 + l := len(files) + wg := new(sync.WaitGroup) + wg.Add(l) + m := new(sync.Mutex) + for _, item := range files { + go func(_item string) { + _ok := self.dumpFont(_item, full) + if _ok { + m.Lock() + ok++ + m.Unlock() + } + wg.Done() + }(item) + } + wg.Wait() + return ok == l +} + +func (self *ass) matchFonts() bool { + if !self.dumpFonts(self.fonts, false) { + return false + } + files, _ := findPath(self._fonts, `\.ttx$`) + reg, _ := regexp.Compile(`_(\d+)\.ttx$`) + for _, item := range files { + f, err := openFile(item, true, false) + if err == nil { + defer f.Close() + names := make([]string, 0) + if xml, err := xmlquery.Parse(f); err == nil { + for _, v := range xml.SelectElements(`ttFont/name/namerecord[@platformID=3]`) { + id := v.SelectAttr("nameID") + name := strings.TrimSpace(v.FirstChild.Data) + switch id { + case "1": + names = append(names, name) + break + case "4": + names = append(names, name) + break + } + } + } + for k, _ := range self.m { + for _, v := range names { + if v == k { + self.m[k].file = reg.ReplaceAllString(item, "") + self.m[k].ttx = item + self.m[k].index = reg.FindStringSubmatch(item)[1] + self.m[k].newName = randomStr(8) + break + } + } + } + } + } + ok := true + for _, v := range self.m { + if v.file == "" { + ok = false + log.Printf(`Missing the font: "%s".`, v.oldName) + } + } + return ok +} + +func (self *ass) createFontSubset(font *fontInfo) bool { + ok := false + fn := fmt.Sprintf(`%s.txt`, font.file) + _, n, e, ne := splitPath(font.file) + if e == ".ttc" { + e = ".ttf" + } + err := os.RemoveAll(self.output) + if !(err == nil || err == os.ErrNotExist) { + log.Println("Failed to clean the output folder.") + return false + } + if os.MkdirAll(self.output, os.ModePerm) != nil { + log.Println("Failed to create the output folder.") + return false + } + if os.WriteFile(fn, []byte(font.str), os.ModePerm) == nil { + _fn := fmt.Sprintf("%s.%s%s", ne, font.newName, e) + _fn = path.Join(self.output, _fn) + args := make([]string, 0) + args = append(args, "--text-file="+fn) + args = append(args, "--output-file="+_fn) + args = append(args, "--name-languages="+"*") + args = append(args, "--font-number="+font.index) + args = append(args, font.file) + if p, err := newProcess(nil, nil, nil, "", pyftsubset, args...); err == nil { + s, err := p.Wait() + ok = err == nil && s.ExitCode() == 0 + } + if !ok { + log.Printf(`Failed to subset font: "%s"[%s].`, n, font.index) + } else { + font.sFont = _fn + } + + } else { + log.Printf(`Failed to write the font text: "%s".`, n) + } + return ok +} + +func (self *ass) createFontsSubset() bool { + ok := 0 + l := len(self.m) + wg := new(sync.WaitGroup) + wg.Add(l) + m := new(sync.Mutex) + for _, item := range self.m { + go func(_item *fontInfo) { + _ok := self.createFontSubset(_item) + if _ok { + m.Lock() + ok++ + m.Unlock() + } + wg.Done() + }(item) + } + wg.Wait() + return ok == l +} + +func (self *ass) changeFontName(font *fontInfo) bool { + ec := 0 + if self.dumpFont(font.sFont, true) { + fn := fmt.Sprintf("%s_0.ttx", font.sFont) + f, err := openFile(fn, true, false) + if err == nil { + defer func() { + _ = f.Close() + _ = os.Remove(fn) + }() + if xml, err := xmlquery.Parse(f); err == nil { + for _, v := range xml.SelectElements(`ttFont/name/namerecord`) { + id := v.SelectAttr("nameID") + switch id { + case "0": + v.FirstChild.Data = "Processed by " + pName + break + case "1", "3", "4", "6": + v.FirstChild.Data = font.newName + break + } + } + str := `<?xml version="1.0" encoding="UTF-8"?>` + str += xml.SelectElement("ttFont").OutputXML(true) + if os.WriteFile(fn, []byte(str), os.ModePerm) == nil { + args := make([]string, 0) + args = append(args, "-q") + args = append(args, "-f") + args = append(args, "-o", font.sFont) + args = append(args, fn) + ok := false + if p, err := newProcess(os.Stdin, nil, nil, "", ttx, args...); err == nil { + s, err := p.Wait() + ok = err == nil && s.ExitCode() == 0 + } + if !ok { + ec++ + _, n, _, _ := splitPath(font.sFont) + log.Printf(`Failed to compile the font: "%s".`, n) + } + } + } else { + log.Printf(`Faild to change the font name: "%s".`, font.oldName) + } + } + } + return ec == 0 +} + +func (self *ass) changeFontsName() bool { + ok := 0 + l := len(self.m) + wg := new(sync.WaitGroup) + wg.Add(l) + m := new(sync.Mutex) + for _, item := range self.m { + go func(_item *fontInfo) { + _ok := self.changeFontName(_item) + if _ok { + m.Lock() + ok++ + m.Unlock() + } + wg.Done() + }(item) + } + wg.Wait() + return ok == l +} + +func (self *ass) replaceFontNameInAss() bool { + ec := 0 + m := make(map[string]map[string]bool) + for _, v := range self.m { + for f, s := range self.subtitles { + if m[f] == nil { + m[f] = make(map[string]bool) + } + n := regEx(v.oldName) + reg, _ := regexp.Compile(fmt.Sprintf(`(Style:[^,\n]+),(@?)%s,`, n)) + s = reg.ReplaceAllString(s, fmt.Sprintf("${1},${2}%s,", v.newName)) + reg, _ = regexp.Compile(fmt.Sprintf(`\\fn(@?)%s`, n)) + s = reg.ReplaceAllString(s, fmt.Sprintf(`\fn${1}%s`, v.newName)) + reg, _ = regexp.Compile(fmt.Sprintf(`(\\fn)?@?%s,?`, n)) + if reg.MatchString(s) { + m[f][v.oldName] = true + } + self.subtitles[f] = s + } + } + for f, s := range self.subtitles { + comments := make([]string, 0) + comments = append(comments, "[script info]") + comments = append(comments, "; ----- Font subset begin -----") + for k, _ := range m[f] { + comments = append(comments, fmt.Sprintf("; Font subset: %s - %s", self.m[k].newName, k)) + } + if len(comments) > 2 { + comments = append(comments, "; Processed by "+pName) + comments = append(comments, "; ----- Font subset end -----") + comments = append(comments, "") + s = strings.Replace(s, "[Script Info]\r\n", strings.Join(comments, "\r\n"), 1) + _, n, _, _ := splitPath(f) + fn := path.Join(self.output, n) + ok := false + if os.WriteFile(fn, []byte(s), os.ModePerm) == nil { + ok = true + } else { + ec++ + } + if !ok { + log.Printf(`Failed to write the new ass file: "%s".`, fn) + } + } + } + return ec == 0 +} + +func genASSes(files []string, fonts, output string) bool { + if len(files) == 0 { + return false + } + obj := new(ass) + obj.files = files + obj._fonts = fonts + obj.output = output + + d, _, _, _ := splitPath(obj.files[0]) + if obj._fonts == "" { + obj._fonts += path.Join(d, "fonts") + } + if obj.output == "" { + obj.output += path.Join(d, "output") + } + + obj.fonts = findFonts(obj._fonts) + + return obj.parse() && obj.matchFonts() && obj.createFontsSubset() && obj.changeFontsName() && obj.replaceFontNameInAss() +} @@ -1,3 +1,17 @@ module mkvtool go 1.17 + +require ( + github.com/antchfx/xmlquery v1.3.7 + github.com/asticode/go-astisub v0.19.0 +) + +require ( + github.com/antchfx/xpath v1.2.0 // indirect + github.com/asticode/go-astikit v0.20.0 // indirect + github.com/asticode/go-astits v1.8.0 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect + golang.org/x/text v0.3.2 // indirect +) @@ -1,41 +1,27 @@ package main import ( - "bytes" - "encoding/json" "flag" "fmt" "os" - "regexp" - "strings" + "runtime" ) -const ( - mkvmerge = `mkvmerge` - mkvextract = `mkvextract` - assfontsubset = `AssFontSubset` -) +const pName = "MKV Tool v3.0.1" -type mkv struct { - Attachments []struct { - ID int `json:"id"` - FileName string `json:"file_name"` - Size int `json:"size"` - ContentType string `json:"content_type"` - } `json:"attachments"` - Tracks []struct { - ID int `json:"id"` - Type string `json:"type"` - Codec string `json:"codec"` - Properties struct { - Language string `json:"language"` - TrackName string `json:"track_name"` - } `json:"properties"` - } +type arrayArg []string + +func (self *arrayArg) String() string { + return fmt.Sprintf("%v", []string(*self)) +} + +func (self *arrayArg) Set(value string) error { + *self = append(*self, value) + return nil } func main() { - setWindowTitle("MKV Tool v2.1.1") + setWindowTitle(pName) s := "" c := false d := false @@ -43,240 +29,67 @@ func main() { n := false q := false sl, st := "", "" + af, ao := "", "" + arr := new(arrayArg) flag.StringVar(&s, "s", "", "Source folder.") flag.BoolVar(&c, "c", false, "Create mode.") flag.BoolVar(&d, "d", false, "Dump mode.") flag.BoolVar(&m, "m", false, "Make mode.") flag.BoolVar(&q, "q", false, "Query mode.") + flag.Var(arr, "a", "ASS files. (multiple & join ass mode)") flag.BoolVar(&n, "n", false, "Not do ass font subset. (dump mode only)") flag.StringVar(&sl, "sl", "chi", " Subtitle language. (create mode only)") flag.StringVar(&st, "st", "", " Subtitle title. (create mode only)") + flag.StringVar(&af, "af", "", " ASS fonts folder. (ASS mode only)") + flag.StringVar(&ao, "ao", "", " ASS output folder. (ASS mode only)") flag.Parse() - if s != "" { + + ec := 0 + if len(*arr) > 0 { + if !genASSes(*arr, af, ao) { + ec++ + } + return + } else if s != "" { if q { - queryFolder(s) + if !queryFolder(s) { + ec++ + } return } if c { if sl != "" { - createMKVs(s, sl, st) + if !createMKVs(s, sl, st) { + ec++ + } return } } if d { - dumpMKVs(s, !n) + if !dumpMKVs(s, !n) { + ec++ + } return } - arr := strings.Split(s, `\`) - p := fmt.Sprintf(`data\%s`, arr[len(arr)-1]) if m { - makeMKVs(s, p) - return - } - dumpMKVs(s, true) - makeMKVs(s, p) - return - } - flag.PrintDefaults() -} - -func getMKVInfo(path string) *mkv { - buf := bytes.NewBufferString("") - p, _ := newProcess(nil, buf, nil, "", mkvmerge, "-J", path) - _, _ = p.Wait() - obj := new(mkv) - _ = json.Unmarshal(buf.Bytes(), obj) - return obj -} - -func dumpMKVs(dir string, subset bool) { - files, _ := findPath(dir, `\.mkv$`) - arr := strings.Split(dir, `\`) - p := fmt.Sprintf(`data\%s`, arr[len(arr)-1]) - l := len(files) - for i, item := range files { - tmp := strings.Replace(item, dir, p, 1) - obj := getMKVInfo(item) - attachments := make([]string, 0) - tracks := make([]string, 0) - for _, _item := range obj.Attachments { - d, _, _, f := splitPath(tmp) - attachments = append(attachments, fmt.Sprintf(`%d:%s`, _item.ID, fmt.Sprintf(`%s%s\fonts\%s`, d, f, _item.FileName))) - } - for _, _item := range obj.Tracks { - if _item.Type == "subtitles" { - d, _, _, f := splitPath(tmp) - s := fmt.Sprintf(`%d_%s_%s`, _item.ID, _item.Properties.Language, _item.Properties.TrackName) - if _item.Codec == "SubStationAlpha" { - s += ".ass" - } else { - s += ".sub" - } - tracks = append(tracks, fmt.Sprintf(`%d:%s`, _item.ID, fmt.Sprintf(`%s%s\%s`, d, f, s))) + if !makeMKVs(s) { + ec++ } + return } - args := make([]string, 0) - args = append(args, item) - args = append(args, "attachments") - args = append(args, attachments...) - args = append(args, "tracks") - args = append(args, tracks...) - p, _ := newProcess(nil, nil, nil, "", mkvextract, args...) - _, _ = p.Wait() - if subset { - asses := make([]string, 0) - for _, _item := range tracks { - _arr := strings.Split(_item, ":") - f := _arr[len(_arr)-1] - if strings.HasSuffix(f, ".ass") { - asses = append(asses, f) - } - if len(asses) > 0 { - p, _ = newProcess(nil, nil, nil, "", assfontsubset, asses...) - _, _ = p.Wait() - } - } - } - fmt.Printf("\rDump (%d/%d) done.", i+1, l) - } -} - -func makeMKVs(dir, dir2 string) { - files, _ := findPath(dir, `\.mkv$`) - arr := strings.Split(dir2, `\`) - p := arr[len(arr)-1] - l := len(files) - for i, item := range files { - tmp := strings.Replace(item, dir, p, 1) - d, _, _, f := splitPath(tmp) - d = strings.Replace(d, p, "", 1) - _p := fmt.Sprintf(`%s%s%s\`, dir2, d, f) - __p := _p + "output" - attachments, _ := findPath(__p, `\.(ttf)|(otf)|(ttc)|(fon)$`) - subs, _ := findPath(_p, `\.sub`) - asses, _ := findPath(__p, `\.ass$`) - tracks := append(subs, asses...) - args := make([]string, 0) - args = append(args, "--output", fmt.Sprintf(`dist\%s`, tmp)) - args = append(args, "--no-subtitles", "--no-attachments") - args = append(args, item) - for _, _item := range attachments { - args = append(args, "--attach-file", _item) - } - for _, _item := range tracks { - _, _, _, f = splitPath(_item) - _arr := strings.Split(f, "_") - args = append(args, "--language", "0:"+_arr[1]) - if len(_arr) > 2 { - args = append(args, "--track-name", "0:"+_arr[2]) - } - args = append(args, _item) - } - p, _ := newProcess(nil, nil, nil, "", mkvmerge, args...) - _, _ = p.Wait() - fmt.Printf("\rMake (%d/%d) done.", i+1, l) - } -} - -func createMKVs(dir string, slang, stitle string) { - v := dir + `\v` - s := dir + `\s` - f := dir + `\f` - t := dir + `\t` - o := dir + `\o` - files, _ := findPath(v, fmt.Sprintf(`\.\S+$`)) - l := len(files) - _ = os.RemoveAll(t) - reg, _ := regexp.Compile(`[\*\.\?\+\$\^\[\]\(\)\{\}\|\\\/]`) - for i, item := range files { - _, _, _, _f := splitPath(item) - _tf := reg.ReplaceAllString(_f, `\$0`) - tmp, _ := findPath(s, fmt.Sprintf(`%s\S*\.\S+$`, _tf)) - asses := make([]string, 0) - subs := make([]string, 0) - p := fmt.Sprintf(`%s\%s\`, t, _f) - for _, sub := range tmp { - if strings.HasSuffix(sub, ".ass") { - _, _, _, __f := splitPath(sub) - __s := fmt.Sprintf(`%s%s.ass`, p, __f) - _ = copyFileOrDir(sub, __s) - asses = append(asses, __s) - } else { - subs = append(subs, sub) - } - } - if len(asses) > 0 { - asses = append([]string{"|f" + f}, asses...) - _p, _ := newProcess(nil, nil, nil, "", assfontsubset, asses...) - _, _ = _p.Wait() - } - __p := fmt.Sprintf(`%s\output`, p) - attachments, _ := findPath(__p, `\.(ttf)|(otf)|(ttc)|(fon)$`) - tracks, _ := findPath(__p, `\.ass$`) - tracks = append(tracks, subs...) - args := make([]string, 0) - args = append(args, "--output", fmt.Sprintf(`%s\%s.mkv`, o, _f)) - args = append(args, item) - for _, _item := range attachments { - args = append(args, "--attach-file", _item) - } - for _, _item := range tracks { - _, _, _, _f = splitPath(_item) - _arr := strings.Split(_f, "_") - _l := len(_arr) - _sl := slang - _st := stitle - if _l > 1 { - _sl = _arr[1] - } - if _l > 2 { - _st = _arr[2] - } - args = append(args, "--language", "0:"+_sl) - args = append(args, "--track-name", "0:"+_st) - args = append(args, _item) - } - _p, _ := newProcess(nil, nil, nil, "", mkvmerge, args...) - _, _ = _p.Wait() - fmt.Printf("\rCreate (%d/%d) done.", i+1, l) - } -} - -func checkSubset(path string) bool { - obj := getMKVInfo(path) - ass := false - ok := false - reg, _ := regexp.Compile(`\.[A-Z0-9]{8}\.\S+$`) - for _, track := range obj.Tracks { - ass = track.Type == "subtitles" && track.Codec == "SubStationAlpha" - if ass { - break - } - } - for _, attachment := range obj.Attachments { - ok = !ass || (strings.HasPrefix(attachment.ContentType, "font/") && reg.MatchString(attachment.FileName)) - if ok { - break + if !dumpMKVs(s, true) { + ec++ + } else if !makeMKVs(s) { + ec++ } + return + } else { + ec++ + flag.PrintDefaults() } - return !ass || (ass && ok) + defer os.Exit(ec) } -func queryFolder(dir string) { - lines := make([]string, 0) - files, _ := findPath(dir, `\.mkv$`) - l := len(files) - for i, file := range files { - if !checkSubset(file) { - lines = append(lines, file) - } - fmt.Printf("\rQuery (%d/%d) done.", i+1, l) - } - if len(lines) > 0 { - fmt.Print("\rHas item(s).") - data := []byte(strings.Join(lines, "\n")) - _ = os.WriteFile("list.txt", data, os.ModePerm) - } else { - fmt.Print("\rNo item.") - } +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) } @@ -0,0 +1,295 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os" + "path" + "regexp" + "strings" +) + +const ( + mkvmerge = `mkvmerge` + mkvextract = `mkvextract` +) + +type mkv struct { + Attachments []struct { + ID int `json:"id"` + FileName string `json:"file_name"` + Size int `json:"size"` + ContentType string `json:"content_type"` + } `json:"attachments"` + Tracks []struct { + ID int `json:"id"` + Type string `json:"type"` + Codec string `json:"codec"` + Properties struct { + Language string `json:"language"` + TrackName string `json:"track_name"` + } `json:"properties"` + } +} + +func getMKVInfo(file string) *mkv { + buf := bytes.NewBufferString("") + if p, err := newProcess(nil, buf, nil, "", mkvmerge, "-J", file); err == nil { + if s, err := p.Wait(); err == nil && s.ExitCode() == 0 { + obj := new(mkv) + _ = json.Unmarshal(buf.Bytes(), obj) + return obj + } + } + return nil +} + +func dumpMKVs(dir string, subset bool) bool { + ec := 0 + files, _ := findPath(dir, `\.mkv$`) + arr := strings.Split(dir, string(os.PathSeparator)) + p := path.Join(`data`, arr[len(arr)-1]) + l := len(files) + for i, item := range files { + tmp := strings.Replace(item, dir, p, 1) + obj := getMKVInfo(item) + if obj == nil { + ec++ + log.Printf(`Failed to get the mkv file info: "%s".`, item) + break + } + attachments := make([]string, 0) + tracks := make([]string, 0) + for _, _item := range obj.Attachments { + d, _, _, f := splitPath(tmp) + attachments = append(attachments, fmt.Sprintf(`%d:%s`, _item.ID, path.Join(d, f, "fonts"+ + "", _item.FileName))) + } + for _, _item := range obj.Tracks { + if _item.Type == "subtitles" { + d, _, _, f := splitPath(tmp) + s := fmt.Sprintf(`%d_%s_%s`, _item.ID, _item.Properties.Language, _item.Properties.TrackName) + if _item.Codec == "SubStationAlpha" { + s += ".ass" + } else { + s += ".sub" + } + tracks = append(tracks, fmt.Sprintf(`%d:%s`, _item.ID, path.Join(d, f, s))) + } + } + args := make([]string, 0) + args = append(args, item) + args = append(args, "attachments") + args = append(args, attachments...) + args = append(args, "tracks") + args = append(args, tracks...) + if p, err := newProcess(nil, nil, nil, "", mkvextract, args...); err == nil { + s, err := p.Wait() + ok := err == nil && s.ExitCode() == 0 + if ok { + if subset { + asses := make([]string, 0) + for _, _item := range tracks { + _arr := strings.Split(_item, ":") + f := _arr[len(_arr)-1] + if strings.HasSuffix(f, ".ass") { + asses = append(asses, f) + } + if len(asses) > 0 { + if !genASSes(asses, "", "") { + ec++ + } + } + } + } + } else { + ec++ + } + } else { + ec++ + } + if ec > 0 { + log.Printf(`Failed to dump the mkv file "%s".`, item) + } + log.Printf("Dump (%d/%d) done.", i+1, l) + } + return ec == 0 +} + +func makeMKVs(dir string) bool { + ec := 0 + _arr := strings.Split(dir, string(os.PathSeparator)) + p := _arr[len(_arr)-1] + dir2 := path.Join(`data`, p) + files, _ := findPath(dir, `\.mkv$`) + l := len(files) + for i, item := range files { + tmp := strings.Replace(item, dir, p, 1) + d, _, _, f := splitPath(tmp) + d = strings.Replace(d, p, "", 1) + _p := path.Join(dir2, d, f) + __p := path.Join(_p, "output") + attachments := findFonts(__p) + subs, _ := findPath(_p, `\.sub`) + asses, _ := findPath(__p, `\.ass$`) + tracks := append(subs, asses...) + args := make([]string, 0) + args = append(args, "--output", path.Join("dist", tmp)) + args = append(args, "--no-subtitles", "--no-attachments") + args = append(args, item) + for _, _item := range attachments { + args = append(args, "--attach-file", _item) + } + for _, _item := range tracks { + _, _, _, f = splitPath(_item) + _arr := strings.Split(f, "_") + args = append(args, "--language", "0:"+_arr[1]) + if len(_arr) > 2 { + args = append(args, "--track-name", "0:"+_arr[2]) + } + args = append(args, _item) + } + if p, err := newProcess(nil, nil, nil, "", mkvmerge, args...); err == nil { + s, err := p.Wait() + ok := err == nil && s.ExitCode() == 0 + if !ok { + ec++ + } + } else { + ec++ + } + if ec > 0 { + log.Printf(`Faild to make the mkv file: "%s".`, item) + } + log.Printf("Make (%d/%d) done.", i+1, l) + } + return ec == 0 +} + +func createMKVs(dir string, slang, stitle string) bool { + ec := 0 + v := path.Join(dir, "v") + s := path.Join(dir, "s") + f := path.Join(dir, "f") + t := path.Join(dir, "t") + o := path.Join(dir, "o") + files, _ := findPath(v, fmt.Sprintf(`\.\S+$`)) + l := len(files) + _ = os.RemoveAll(t) + for i, item := range files { + _, _, _, _f := splitPath(item) + _tf := reg.ReplaceAllString(_f, `\$0`) + tmp, _ := findPath(s, fmt.Sprintf(`%s\S*\.\S+$`, _tf)) + asses := make([]string, 0) + subs := make([]string, 0) + p := path.Join(t, _f) + for _, sub := range tmp { + if strings.HasSuffix(sub, ".ass") { + _, _, _, __f := splitPath(sub) + __s := path.Join(p, __f) + ".ass" + _ = copyFileOrDir(sub, __s) + asses = append(asses, __s) + } else { + subs = append(subs, sub) + } + } + if len(asses) > 0 { + if !genASSes(asses, f, "") { + ec++ + } + } + __p := path.Join(p, "output") + attachments := findFonts(__p) + tracks, _ := findPath(__p, `\.ass$`) + tracks = append(tracks, subs...) + args := make([]string, 0) + args = append(args, "--output", path.Join(o, _f)+".mkv") + args = append(args, item) + for _, _item := range attachments { + args = append(args, "--attach-file", _item) + } + for _, _item := range tracks { + _, _, _, _f = splitPath(_item) + _arr := strings.Split(_f, "_") + _l := len(_arr) + _sl := slang + _st := stitle + if _l > 1 { + _sl = _arr[1] + } + if _l > 2 { + _st = _arr[2] + } + args = append(args, "--language", "0:"+_sl) + args = append(args, "--track-name", "0:"+_st) + args = append(args, _item) + } + if p, err := newProcess(nil, nil, nil, "", mkvmerge, args...); err == nil { + s, err := p.Wait() + ok := err == nil && s.ExitCode() == 0 + if !ok { + ec++ + } + } else { + ec++ + } + if ec > 0 { + log.Printf(`Failed to create the mkv file: "%s".`, item) + } + log.Printf("Create (%d/%d) done.", i+1, l) + } + return ec == 0 +} + +func checkSubset(path string) (bool, bool) { + obj := getMKVInfo(path) + if obj == nil { + log.Printf(`Failed to get the mkv file info: "%s".`, path) + return false, true + } + ass := false + ok := false + reg, _ := regexp.Compile(`\.[A-Z0-9]{8}\.\S+$`) + for _, track := range obj.Tracks { + ass = track.Type == "subtitles" && track.Codec == "SubStationAlpha" + if ass { + break + } + } + for _, attachment := range obj.Attachments { + ok = !ass || (strings.HasPrefix(attachment.ContentType, "font/") && reg.MatchString(attachment.FileName)) + if ok { + break + } + } + return !ass || (ass && ok), false +} + +func queryFolder(dir string) bool { + ec := 0 + lines := make([]string, 0) + files, _ := findPath(dir, `\.mkv$`) + l := len(files) + for i, file := range files { + a, b := checkSubset(file) + if b { + ec++ + } else if !a { + lines = append(lines, file) + } + log.Printf("Query (%d/%d) done.", i+1, l) + } + if len(lines) > 0 { + fmt.Print("Has item(s).") + data := []byte(strings.Join(lines, "\n")) + if os.WriteFile("list.txt", data, os.ModePerm) != nil { + log.Printf(`Faild to write the dir result file: "%s".`, dir) + ec++ + } + } else { + fmt.Print("No item.") + } + return ec == 0 +} diff --git a/title.go b/title.go new file mode 100644 index 0000000..a67e1ec --- /dev/null +++ b/title.go @@ -0,0 +1,9 @@ +//go:build !windows + +package main + +import "fmt" + +func setWindowTitle(title string) { + fmt.Printf("\033]0;%s\007", title) +} diff --git a/title_windows.go b/title_windows.go new file mode 100644 index 0000000..b53fe23 --- /dev/null +++ b/title_windows.go @@ -0,0 +1,22 @@ +//go:build windows + +package main + +import ( + "syscall" + "unsafe" +) + +func setWindowTitle(title string) { + kernel32, err := syscall.LoadLibrary("kernel32.dll") + if err == nil { + defer syscall.FreeLibrary(kernel32) + setConsoleTitle, err := syscall.GetProcAddress(kernel32, "SetConsoleTitleW") + if err == nil { + ptr, err := syscall.UTF16PtrFromString(title) + if err == nil { + syscall.Syscall(setConsoleTitle, 1, uintptr(unsafe.Pointer(ptr)), 0, 0) + } + } + } +} @@ -4,13 +4,14 @@ import ( "errors" "fmt" "io" + "math/rand" "os" "os/exec" + "path" "path/filepath" "regexp" "strings" - "syscall" - "unsafe" + "time" ) func newProcess(stdin io.Reader, stdout, stderr io.Writer, dir, prog string, args ...string) (p *os.Process, err error) { @@ -34,20 +35,6 @@ func newProcess(stdin io.Reader, stdout, stderr io.Writer, dir, prog string, arg return } -func setWindowTitle(title string) { - kernel32, err := syscall.LoadLibrary("kernel32.dll") - if err == nil { - defer syscall.FreeLibrary(kernel32) - setConsoleTitle, err := syscall.GetProcAddress(kernel32, "SetConsoleTitleW") - if err == nil { - ptr, err := syscall.UTF16PtrFromString(title) - if err == nil { - syscall.Syscall(setConsoleTitle, 1, uintptr(unsafe.Pointer(ptr)), 0, 0) - } - } - } -} - func newDir(path string) error { return os.MkdirAll(path, os.ModePerm) } @@ -127,13 +114,13 @@ func newFile(fp string) (file *os.File, err error) { return } -func openFile(filepath string, readOnly bool) (file *os.File, err error) { - f := os.O_RDWR +func openFile(filepath string, readOnly, create bool) (file *os.File, err error) { + f := os.O_RDWR | os.O_CREATE if readOnly { f = os.O_RDONLY } file, err = os.OpenFile(filepath, f, os.ModePerm) - if err != nil { + if err != nil && create { file, err = newFile(filepath) } return @@ -149,14 +136,14 @@ func copyFile(src, dst string) error { } if _, n, _, _ := splitPath(dst); n == "" { _, n, _, _ = splitPath(src) - dst = fmt.Sprintf("%s/%s", dst, n) + dst = path.Join(dst, n) } - sf, err := openFile(src, true) + sf, err := openFile(src, true, false) if err != nil { return err } defer sf.Close() - df, err := openFile(dst, false) + df, err := openFile(dst, false, true) if err != nil { return err } @@ -195,3 +182,40 @@ func copyFileOrDir(src, dst string) error { } return copyFolder(src, dst) } + +func randomStr(l int) string { + str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + bytes := []byte(str) + var result []byte + lstr := len(str) - 1 + for i := 0; i < l; i++ { + n := randomNumber(0, lstr) + result = append(result, bytes[n]) + } + return string(result) +} + +var r = rand.New(rand.NewSource(time.Now().UnixNano())) + +func randomN(n int) int { + return r.Intn(n) +} + +func randomNumber(min, max int) int { + sub := max - min + 1 + if sub <= 1 { + return min + } + return min + randomN(sub) +} + +var reg, _ = regexp.Compile(`[\*\.\?\+\$\^\[\]\(\)\{\}\|\\\/]`) + +func regEx(str string) string { + return reg.ReplaceAllString(str, `\$0`) +} + +func findFonts(dir string) []string { + list, _ := findPath(dir, `\.((ttf)|(otf)|(ttc)|(fon))$`) + return list +} |
