package main import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" ) // maps user input to events type logicMatchEntry struct { event func(string) match *regexp.Regexp } // compile regex patterns and map to actions var ( logicMatchTable = map[string]*logicMatchEntry{ "logicSearch": &logicMatchEntry{logicSearch, regexp.MustCompile(`(?m)^[^\:]`)}, "logicNumber": &logicMatchEntry{logicNumber, regexp.MustCompile(`(?m)^\:[0-9]+(\ |$)`)}, "logicPage": &logicMatchEntry{logicPage, regexp.MustCompile(`(?m)^\::[0-9]+$`)}, "logicSub": &logicMatchEntry{logicSub, regexp.MustCompile(`(?m)^\:sub$`)}, // avoid matching conflict with subadd! "logicSubAdd": &logicMatchEntry{logicSubAdd, regexp.MustCompile(`(?m)^\:subadd`)}, "logicSubDel": &logicMatchEntry{logicSubDel, regexp.MustCompile(`(?m)^\:subdel`)}, "logicUser": &logicMatchEntry{logicUser, regexp.MustCompile(`(?m)^\:user`)}, "logicNew": &logicMatchEntry{logicNew, regexp.MustCompile(`(?m)^\:new$`)}, "logicSet": &logicMatchEntry{logicSet, regexp.MustCompile(`(?m)^\:set`)}, "logicHelp": &logicMatchEntry{logicHelp, regexp.MustCompile(`(?m)^\:help`)}, "logicQuit": &logicMatchEntry{logicQuit, regexp.MustCompile(`(?m)^\:q$`)}, } ) type logicWindowState int // window states const ( logicWindowNone logicWindowState = iota logicWindowSearch logicWindowSub logicWindowUser ) var ( logicVarWindow = logicWindowNone logicVarVideos []*videoContainer = []*videoContainer{} logicVarLazy = &listLazy{} logicVarUsers = []string{} logicVarUser = "" logicVarSearch = "" logicVarCurrentPage = 0 ) // logicAction parses the input string and performs the given action func logicAction(input string) { // match pattern logicMatch(input)(logicTrim(input)) } // logicMatch returns the best fitting function for the given input func logicMatch(input string) func(string) { for _, entry := range logicMatchTable { if entry.match.MatchString(input) { return entry.event } } return logicUnknown } // logicTrim separates and returns the commands arguments func logicTrim(trim string) string { if logicMatchTable["logicNumber"].match.MatchString(trim) { return trim[1:] } if logicMatchTable["logicPage"].match.MatchString(trim) { return trim[2:] } if strings.Index(trim, ":") == 0 { out := strings.SplitN(trim, " ", 2) if len(out) > 1 { return out[1] } return "" } return trim } // logicOnWinch reloads the page if the window size was changed func logicOnWinch() { cliClear() logicLoadPage(logicVarCurrentPage) cliHome() } // logicSearch searches for the given video title func logicSearch(title string) { upd := func(state int) []*videoContainer { return videoListSearch(title, strconv.Itoa(state+1)) } logicVarLazy = listNew(upd) logicSearchPage("searching: "+title, 0) } //logicSearchPage searches the title in the given page func logicSearchPage(title string, page int) { cliStatus("page " + strconv.Itoa(page) + " | " + title) logicVarWindow = logicWindowSearch logicVarSearch = title linecount := termLines() - 2 list := logicVarLazy.listSlice(page*linecount, (page+1)*linecount) if list == nil { cliStatusError("network error: unable to load data") return } if len(list) == 0 { cliStatusError("no results for the given query") } logicList(-1, list) } // logicNumber selects the given list number func logicNumber(command string) { splits := strings.SplitN(command, " ", 2) player := logicGetPlayer() if len(splits) > 1 { player = splits[1] } sel, err := strconv.Atoi(splits[0]) errorCare(err) switch logicVarWindow { case logicWindowNone: cliStatusError("error: video list is empty") case logicWindowUser: fallthrough case logicWindowSearch: if sel < len(logicVarVideos) { logicPlay(player, sel) // non blocking } else { cliStatusError("error: video id not in list") } case logicWindowSub: if sel < len(logicVarUsers) { logicUser(logicVarUsers[sel]) } else { cliStatusError("error: user id not in list") } } } // logicPage selects the given page func logicPage(page string) { pnum, err := strconv.Atoi(page) errorCare(err) logicLoadPage(pnum) } // logicLoadPage calls the relevant function according to the // window state func logicLoadPage(pnum int) { logicVarCurrentPage = pnum // save for list redraw switch logicVarWindow { case logicWindowSearch: logicSearchPage(logicVarSearch, pnum) case logicWindowUser: logicUserPage(logicVarUser, pnum) } } // logicGetUsers retieves a list of all subscribed users func logicGetUsers() []string { subs, err := storeReadFile(storeDefaultSubscriptions) errorCare(err) users := []string{} for _, user := range strings.Split(subs, "\n") { if len(user) > 2 && user[0] == '"' && user[len(user)-1] == '"' { users = append(users, user[1:len(user)-1]) } } return users } // logicSub lists the subscriptions func logicSub(none string) { columns := 4 logicVarWindow = logicWindowSub id := make([][]string, columns) us := make([][]string, columns) users := logicGetUsers() sort.Strings(users) newcol := len(users)/columns + 1 for i, u := range users { id[i/newcol] = append(id[i/newcol], strconv.Itoa(i)) us[i/newcol] = append(us[i/newcol], u) } logicVarUsers = users cliStatus("list of subscriptions") cliTable(-1, id[0], us[0], id[1], us[1], id[2], us[2], id[3], us[3]) } // logicSubAdd adds the given user to the subscription list func logicSubAdd(user string) { if user == "" && logicVarWindow == logicWindowUser { user = logicVarUser } if strings.Contains(user, "\"") { cliStatusError("error: illicit username") return } subs, err := storeReadFile(storeDefaultSubscriptions) errorCare(err) if strings.Contains(subs, "\""+user+"\"") { cliStatusError("error: user '" + user + "' already in list") return } errorCare(storeWriteFile(storeDefaultSubscriptions, subs+"\""+user+"\"\n")) cliStatus("subscription '" + user + "' added") cliClearDown() } // logicSubAdd adds the given user to the subscription list func logicSubDel(user string) { if user == "" && logicVarWindow == logicWindowUser { user = logicVarUser } if strings.Contains(user, "\"") { cliStatusError("error: illicit username") return } subs, err := storeReadFile(storeDefaultSubscriptions) errorCare(err) if !strings.Contains(subs, "\""+user+"\"") { cliStatusError("error: user '" + user + "' not in list") return } errorCare(storeWriteFile(storeDefaultSubscriptions, strings.Replace(subs, "\""+user+"\"\n", "", 1))) // meh cliStatus("subscription '" + user + "' deleted") cliClearDown() } // logicUser shows the videos of the given user func logicUser(user string) { logicUserPage(user, 0) } // logicUserPage shows the page of the given user func logicUserPage(user string, page int) { // TODO: support pages (priority: low) cliStatus("page " + strconv.Itoa(page) + " | user: " + user) logicVarWindow = logicWindowUser logicVarUser = user logicList(-1, videoListUser(user)) } // logicNew shows a list of new videos func logicNew(none string) { logicVarWindow = logicWindowSearch logicVarSearch = "" users := logicGetUsers() upd := func(state int) []*videoContainer { if state < len(users) { return videoListUser(users[state]) } return nil // all users updated (wont occur in reality) } logicVarLazy = listNew(upd) cliStatus("updating users ...") logicVarLazy.listPar(len(users)) logicVarLazy.listGet(0) // downloads all user pages as par = len(users) cliHome() cliPrintln() cliStatus("sorting ...") sort.Sort(&videoSort{logicVarLazy.listCached()}) logicSearchPage("new videos", 0) } // logicSet sets the key to the given value. Format: "key value" func logicSet(perform string) { logicSetPlayer(perform) } // logicHelp shows the help func logicHelp(none string) { cliStatus("available commands and usage") cliClearDown() help := ` to search for a video input keywords commands: :<number> [player] select list item (optional: video player/script*) ::<page_number> show the specified page :user <username> show videos of the given user :sub show subscriptions :subadd <username> subscribe to the given or current user :subdel <username> delete subscription of the given or current user :new show newest videos of subscribed users :set <player> set the default video player :help show this help :q quit the program *Scripts placed in the ~/.config/tubus/scripts directory are preferred to other executable programs in $PATH. The video url will be passed as first command-line argument. ` cliPrintln(strings.Replace(help, "\n", "\r\n", -1)) } // logicUnknown shows a message if the command could not be parsed func logicUnknown(command string) { cliStatusError("error: unknown command") } // logicQuit exits the program func logicQuit(none string) { cliReset() termClose() os.Exit(0) } // logicList shows a video list and highlights the line with the given // line number. If hightlight < 0 no line will be highlighted. // The global video list logicVarVideos will be set by this function. func logicList(highlight int, videos []*videoContainer) { logicVarVideos = videos cliClearDown() id, titles, urls, durations, users, dates, views := []string{}, []string{}, []string{}, []string{}, []string{}, []string{}, []string{} for i, v := range videos { id = append(id, strconv.Itoa(i)) titles = append(titles, cliTrimLength(v.title, " >>", 55)) urls = append(urls, v.url) durations = append(durations, v.duration) users = append(users, v.user) dates = append(dates, v.date) views = append(views, v.views) } cliTable(highlight, id, titles, durations, users, views, dates) } // logicGetPlayer gets the default video player func logicGetPlayer() string { player, err := storeReadFile(storeDefaultPlayer) errorCare(err) return strings.Replace(player, "\n", "", -1) } // logicSetPlayer sets the default video player func logicSetPlayer(player string) { errorCare(storeWriteFile(storeDefaultPlayer, player)) cliStatus("default player set to '" + player + "'") cliClearDown() } // logicPlay plays the video with the given video id in the player func logicPlay(player string, id int) { video := logicVarVideos[id] cmd := append(strings.Split(player, " "), "https://www.youtube.com/watch?v="+video.url) script := filepath.Join(storeDefaultScripts, cmd[0]) exists, err := storeFileExists(script) errorCare(err) if exists { cmd[0] = script player = "$" + player // visual indicator for user script } exe := exec.Command(cmd[0], cmd[1:]...) if err := exe.Start(); err != nil { cliStatusError(fmt.Sprint("playback error: ", err)) } else { cliStatus("[" + player + "] " + video.title) logicList(id, logicVarVideos) } go exe.Wait() }