Skip to content

Instantly share code, notes, and snippets.

@mmstick
Last active November 23, 2017 00:12
Show Gist options
  • Save mmstick/3182c1c8596c1f830c7e to your computer and use it in GitHub Desktop.
Save mmstick/3182c1c8596c1f830c7e to your computer and use it in GitHub Desktop.
Transcodes all episodes in the current directory and all of it's subdirectories into H.265 + Opus MKVs using ffmpeg. Make sure to have a copy of ffmpeg installed that can transcode to H.265 + Opus, preferably one that uses H.265 10-bit.
package main
import "flag"
import "fmt"
import "io/ioutil"
import "os"
import "os/exec"
import "time"
const (
CRF_HELP = "Set the CRF value: 0-51. Higher CRF gives lower quality."
STATIC_PARAMS = "me=star:subme=7:bframes=16:b-adapt=2:ref=16:rc-lookahead=60:max-merge=5:tu-intra-depth=4:tu-inter-depth=4"
)
var (
crf = flag.Int("crf", 24, CRF_HELP)
sprintf = fmt.Sprintf
)
// Returns a list of files in the current directory
func scanDirectory(path string) []os.FileInfo {
directory, _ := ioutil.ReadDir(path)
return directory
}
// Contains information about each episode
type Episode struct {
name string
directory string
originalSize int64
transcodedSize int64
sizeDifference int64
time time.Duration
stat error
}
// Returns the encode parameters for the episode
func encodeParameters(inputName, outputName *string) *exec.Cmd {
x265Parameters := sprintf("crf=%d:%s", *crf, STATIC_PARAMS)
return exec.Command("ffmpeg", "-i", *inputName, "-c:a", "libopus",
"-c:v", "libx265", "-x265-params",
x265Parameters, *outputName)
}
// Returns the size of the newly transcoded episode, if it exists.
func transcodeSize(transcodedEpisode *string) int64 {
file, err := os.Open(*transcodedEpisode)
if err == nil {
stat, _ := file.Stat()
return stat.Size() / 1000000
} else {
return 0
}
}
// Transcode the current episode
func (ep Episode) transcode(index, files int) {
inputName, outputName := getFileNames(&ep.directory, &ep.name)
fmt.Printf("Transcoding %d/%d: %s\n", index, files, ep.name)
startTime := time.Now()
ep.stat = encodeParameters(&inputName, &outputName).Run()
ep.time = time.Since(startTime)
ep.transcodedSize = transcodeSize(&outputName)
ep.sizeDifference = ep.originalSize - ep.transcodedSize
}
// Print the status of the transcoded episode
func (episode Episode) status() {
if episode.stat != nil {
fmt.Println("Failed to transcode", episode.name)
} else {
fmt.Println("Transcoded", episode.name)
fmt.Println("Original Size:", episode.originalSize, "MB")
fmt.Println("New Size:", episode.transcodedSize, "MB")
fmt.Println("Difference:", episode.sizeDifference, "MB")
fmt.Println("Time:", episode.time)
}
}
// Replaces the file extension from the input string and changes it to mkv
func convertToMKV(input string) string {
for index := len(input) - 1; index >= 0; index-- {
if input[index] == '.' {
input = input[:index]
}
}
return input + ".mkv"
}
// Returns the input name and output name for the file to be encoded
func getFileNames(directory, name *string) (string, string) {
inputName := sprintf("%s/%s", *directory, *name)
outputName := convertToMKV(sprintf("%s/encoded_%s", *directory, *name))
return inputName, outputName
}
// If the file is a directory, recurse through the directory.
func recurseDirectory(directory *string, filename string) {
subdirectory := sprintf("%s/%s", *directory, filename)
scanEpisodes(scanDirectory(subdirectory), subdirectory)
}
// Append the current episode to the episode list.
func appendEpisode(list *[]Episode, file os.FileInfo, directory *string) {
*list = append(*list, Episode{
name: file.Name(),
directory: *directory,
originalSize: file.Size() / 1000000,
})
}
// Recurse through each subdirectory and adds each episode to the episode list
func scanEpisodes(directoryList []os.FileInfo, directory string) *[]Episode {
list := []Episode{}
for _, file := range directoryList {
if file.IsDir() {
recurseDirectory(&directory, file.Name())
} else {
appendEpisode(&list, file, &directory)
}
}
return &list
}
// Transcodes all episodes in the episode list
func transcodeEpisodes(episodeList *[]Episode) {
files := len(*episodeList)
for index, ep := range *episodeList {
ep.transcode(index+1, files)
ep.status()
}
}
func main() {
flag.Parse()
startTime := time.Now()
directory, _ := os.Getwd()
transcodeEpisodes(scanEpisodes(scanDirectory(directory), directory))
fmt.Printf("Transcoding completed in %s\n", time.Since(startTime))
}
@suntong
Copy link

suntong commented May 31, 2015

Thanks for sharing!
I'd reverse the function order though, to put main at the top then layout functions top down. That's the beauty of Go over C. my 2c.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment