package main import ( "crypto/rand" "encoding/hex" "flag" "fmt" "os" "os/signal" "path/filepath" "regexp" "syscall" "videnc-vibe/internal/config" "videnc-vibe/internal/encoder" "videnc-vibe/internal/logger" "videnc-vibe/internal/metadata" "videnc-vibe/internal/mover" "videnc-vibe/internal/watcher" "videnc-vibe/pkg/types" ) var ( deleteOrigin bool configPath string ) func init() { flag.BoolVar(&deleteOrigin, "d", false, "Delete original after successful encode") flag.StringVar(&configPath, "c", "", "Config file path (default: ~/.config/videnc-vibe/config.yaml)") } func main() { flag.Parse() cfg, err := config.Load(configPath) if err != nil { fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err) os.Exit(1) } if err := config.EnsureDirs(cfg); err != nil { fmt.Fprintf(os.Stderr, "Failed to create directories: %v\n", err) os.Exit(1) } log, err := logger.New(".") if err != nil { fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) os.Exit(1) } defer log.Close() enc := encoder.New() if err := enc.CheckDeps(); err != nil { log.Error("Dependency check failed", err.Error()) fmt.Fprintf(os.Stderr, "Dependency check failed: %v\n", err) os.Exit(1) } metaClient := metadata.NewClient(cfg.OMDBAPIKey) done := make(chan struct{}) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan close(done) }() w := watcher.New(cfg.Paths.Input, 15) w.Start(func(inputPath string) error { return processFile(inputPath, cfg, enc, metaClient, log) }, done) log.Info("videnc-vibe started") } func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, metaClient *metadata.Client, log *logger.Logger) error { log.Info(fmt.Sprintf("Processing: %s", inputPath)) filename := filepath.Base(inputPath) isSeries, imdbID, tvmazeID, season, episode := metadata.ParseFilename(filename) width, height, interlaced, err := enc.GetMediaInfo(inputPath) if err != nil { log.ErrorFile(inputPath, "Getting media info", err.Error()) mover.MoveToFailed(inputPath, cfg.Paths.Failed) return err } streamLangs, err := enc.GetStreamLanguages(inputPath) if err != nil { log.ErrorFile(inputPath, "Getting stream languages", err.Error()) } mediaType := metadata.ParseMediaType(filename) if mediaType == "" { mediaType = watcher.DetectMediaType(width, height) } var profile types.EncodingParams switch mediaType { case types.MediaTypeBluRay: profile = cfg.Encoding.Bluray case types.MediaTypeWebDL: profile = cfg.Encoding.WebDL case types.MediaTypeTVRip: profile = cfg.Encoding.TVRip default: profile = cfg.Encoding.DVD } crf := profile.CRF preset := profile.Preset var meta *types.Metadata if isSeries && tvmazeID != "" && season != "" && episode != "" { meta, err = metaClient.FetchSeriesMetadata(tvmazeID, season, episode) if err != nil { log.ErrorFile(inputPath, "Fetching series metadata", err.Error()) } } else if imdbID != "" { meta, err = metaClient.FetchMovieMetadata(imdbID) if err != nil { log.ErrorFile(inputPath, "Fetching movie metadata", err.Error()) } } if meta == nil { meta = &types.Metadata{ Title: "Unknown", DateReleased: "", IMDBID: "", OriginalMedia: mediaType, } } meta.OriginalMedia = mediaType job := &types.Job{ InputPath: inputPath, MediaType: mediaType, CRF: crf, Preset: preset, DeleteOrigin: deleteOrigin, } if err := enc.Transcode(inputPath, job, meta, interlaced, streamLangs); err != nil { log.ErrorFile(inputPath, "Transcoding", err.Error()) mover.MoveToFailed(inputPath, cfg.Paths.Failed) outputPath := filepath.Join(filepath.Dir(inputPath), "output.mkv") mover.MoveToFailed(outputPath, cfg.Paths.Failed) return err } outputDir := filepath.Dir(inputPath) outputPath := filepath.Join(outputDir, "output.mkv") var outFilename string switch { case isSeries && meta.Collection != "": outFilename = fmt.Sprintf("%s.S%sE%s.mkv", sanitizeFilename(meta.Collection), season, episode) case !isSeries && meta.IMDBID != "" && meta.Title != "Unknown": outFilename = fmt.Sprintf("%s.%s.mkv", sanitizeFilename(meta.Title), meta.IMDBID) default: outFilename = fmt.Sprintf("%s.nometadata.mkv", generateRandomString(8)) } finalOutput := filepath.Join(cfg.Paths.Output, outFilename) if err := mover.Rename(outputPath, finalOutput); err != nil { log.ErrorFile(inputPath, "Moving output", err.Error()) mover.MoveToFailed(outputPath, cfg.Paths.Failed) return err } if deleteOrigin { mover.Delete(inputPath) log.Info(fmt.Sprintf("Deleted original: %s", inputPath)) } else { mover.MoveToOriginals(inputPath, cfg.Paths.Originals) log.Info(fmt.Sprintf("Moved original to originals: %s", inputPath)) } log.Info(fmt.Sprintf("Completed: %s -> %s", inputPath, finalOutput)) return nil } func generateRandomString(length int) string { bytes := make([]byte, length/2+1) rand.Read(bytes) return hex.EncodeToString(bytes)[:length] } func sanitizeFilename(name string) string { reg := regexp.MustCompile(`[^a-zA-Z0-9\-äöÄÖ]`) return reg.ReplaceAllString(name, "") }