Commit 01dc0aef authored by Jonny Schäfer's avatar Jonny Schäfer
Browse files

Initial Commit

parents
Pipeline #171 skipped
package main
import (
"fmt"
"golang.org/x/text/width"
"strings"
"unicode"
)
// cliLineWrap enables or disables line wrapping
func cliLineWrap(enabled bool) {
if enabled {
fmt.Print("\x1b[?7h")
} else {
fmt.Print("\x1b[?7l")
}
}
// cliReset resets the terminal to the defaults
func cliReset() {
cliLineWrap(true)
fmt.Print("\x1bc")
}
// cliClear clears the screen
func cliClear() {
cliHome()
cliClearDown()
cliHome()
}
// cliClear clears the screen downwards beginning at the cursor position
func cliClearDown() {
fmt.Print("\x1b[J")
}
// cliClear clears the screen right beginning at the cursor position
func cliClearRight() {
fmt.Print("\x1b[K")
}
// cliHome sets the cursor to the origin
func cliHome() {
fmt.Print("\x1b[;H")
}
// cliPrompt reads a line from stdin
func cliPrompt() string {
cliHome()
read, err := readline()
if err != nil {
logicQuit("EOF")
}
cliHome()
cliClearRight() // erase input
return read
}
// cliStatus writes the current status to the screen
func cliStatus(status string) {
cliHome()
fmt.Println()
cliClearRight() // erase status
fmt.Println("\x1b[44;37m" + status + "\x1b[0m")
}
// cliStatus writes the current error status to the screen
func cliStatusError(status string) {
cliHome()
fmt.Println()
cliClearRight() // erase status
fmt.Println("\x1b[1;41;37m" + status + "\x1b[0m")
}
// cliList writes a table to the screen
func cliTable(columns ...[]string) {
cliClearDown()
space := make([]int, len(columns))
lines := 0
for i, c := range columns {
if len(c) > lines { // determine longest coloumn
lines = len(c)
}
for _, s := range c {
lens := cliRealCount(s) // determine longest line per coloumn
if lens > space[i] {
space[i] = lens
}
}
}
for i := 0; i < lines; i++ {
for n, c := range columns {
fmt.Printf("\x1b[%vm", ((n+1)%2)*34)
if i < len(c) {
fmt.Print(c[i] + strings.Repeat(" ", 1+space[n]-cliRealCount(c[i]))) // not good
} else {
fmt.Print(strings.Repeat(" ", 1+space[n]))
}
}
fmt.Println()
}
fmt.Print("\x1b[0m")
}
// cliTrimLength trims the given text to the rune count and indicates
// it by appending an indicator suffix
func cliTrimLength(text, indicator string, count int) string {
indl := cliRealCount(indicator)
show := text
for cliRealCount(show)+indl > count {
show = show[:(len(show) - 1)]
}
if show != text {
return show + indicator
}
return show
}
// cliRealCount hopefully counts the visible length of a string
func cliRealCount(in string) (glyphs int) {
for _, r := range in {
if unicode.IsMark(r) {
continue
}
glyphs++
if width.LookupRune(r).Kind() == width.EastAsianWide { // TODO might break in some terminals!
glyphs++
}
}
return
}
package main
import (
"log"
)
// errorCare exits the program with a message if an error occured
func errorCare(err error) {
if err != nil {
cliReset()
log.Fatal("An Error occured: ", err, "\n\nI am Sorry :(")
}
}
package main
import (
"fmt"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
"sync"
)
// 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{}
logicVarUsers = []string{}
logicVarUser = ""
logicVarSearch = ""
)
// 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
}
// logicSearch searches for the given video title
func logicSearch(title string) {
logicSearchPage(title, "1")
}
//logicSearchPage searches the title in the given page
func logicSearchPage(title, page string) {
cliStatus("page " + page + " | searching for: " + title)
logicVarWindow = logicWindowSearch
logicVarSearch = title
list := videoListSearch(title, page)
if list == nil {
cliStatusError("network error: unable to load data")
return
}
if len(list) == 0 {
cliStatusError("no results for the given query")
}
logicList(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, logicVarVideos[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) {
switch logicVarWindow {
case logicWindowSearch:
logicSearchPage(logicVarSearch, page)
case logicWindowUser:
logicUserPage(logicVarUser, page)
}
}
// logicGetUsers retieves a list of all subscripted 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(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, "1")
}
// logicUserPage shows the page of the given user
func logicUserPage(user, page string) {
// TODO: support pages (priority: low)
cliStatus("page " + page + " | user: " + user)
logicVarWindow = logicWindowUser
logicVarUser = user
logicList(videoListUser(user))
}
// logicNew shows a list of new videos
func logicNew(none string) {
logicVarWindow = logicWindowSearch
logicVarSearch = ""
videos := []*videoContainer{}
users := logicGetUsers()
lock := &sync.Mutex{}
barrier := &sync.WaitGroup{}
barrier.Add(len(users))
for i, user := range users {
// put into scope
user := user
i := i
go func() {
list := videoListUser(user)
cliHome()
fmt.Println()
lock.Lock()
cliStatus("updating users (" + strconv.Itoa(i+1) + "/" + strconv.Itoa(len(users)) + ")")
videos = append(videos, list...)
lock.Unlock()
barrier.Done()
}()
}
barrier.Wait()
cliHome()
fmt.Println()
cliStatus("sorting ...")
sort.Sort(&videoSort{videos})
if len(videos) > 30 {
videos = videos[0:30] // TODO add multiple pages
}
logicVarVideos = videos
cliHome()
fmt.Println()
cliStatus("newest videos")
logicList(videos)
}
// 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 simply input keywords
commands:
:[number] [player] select list item (optional: video player)
::[number] select list page
:user [user] show videos of user
:sub show subscriptions
:new show new subscribed videos
:subadd [user] add given or current user
:subdel [user] delete given or current user
:set [player] set default video player
:help show this help
:q quit the program
`
fmt.Println(help)
}
// 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()
os.Exit(0)
}
// logicList shows a video list
func logicList(videos []*videoContainer) {
cliClearDown()
logicVarVideos = videos
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(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 in the given player
func logicPlay(player string, video *videoContainer) {
cmd := append(strings.Split(player, " "), "https://www.youtube.com/watch?v="+video.url)
exe := exec.Command(cmd[0], cmd[1:]...)
if err := exe.Start(); err != nil {
cliStatusError(fmt.Sprint("playback error: ", err))
} else {
cliStatus("[" + player + "] " + video.title)
}
go exe.Wait()
}
package main
// This file uses cgo. The following comment block contains c code, that
// is actually in use. Do not add newlines above the import statement!
// See cgo manual for additional information.
/*
#cgo LDFLAGS: -lreadline
#include <stdio.h>
#include <stdlib.h>
#include <readline/readline.h>
#include <readline/history.h>
char* readempty() {
return readline("");
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func readline() (string, error) {
cline := C.readempty()
if cline != nil {
gline := C.GoString(cline)
C.free(unsafe.Pointer(cline))
return gline, nil
}
return "", fmt.Errorf("EOF?")
}
package main
import (
"io/ioutil"
"os"
"os/user"
"path/filepath"
)
var storeFile = "" // TODO: User Directory
var (
storeDefaultPlayer = ""
storeDefaultSubscriptions = ""
)
// init checks if the directories exist and creates them if necessary
func init() {
user, err := user.Current()
errorCare(err)
home := user.HomeDir
conf := filepath.Join(home, ".config")
exists, err := storeFileExists(conf)
errorCare(err)
if !exists {
errorCare(storeCreateDirectory(conf))
}
conf = filepath.Join(conf, "tubus")
exists, err = storeFileExists(conf)
errorCare(err)
if !exists {
errorCare(storeCreateDirectory(conf))
}
storeDefaultSubscriptions = filepath.Join(conf, "subscriptions")
exists, err = storeFileExists(storeDefaultSubscriptions)
if !exists {
errorCare(storeCreateFile(storeDefaultSubscriptions))
}
storeDefaultPlayer = filepath.Join(conf, "player")
exists, err = storeFileExists(storeDefaultPlayer)
if !exists {
errorCare(storeCreateFile(storeDefaultPlayer))
}
}
// storeCreateDirectory creates an empty file
func storeCreateFile(path string) error {
file, err := os.Create(path)
file.Close()
return err
}
// storeCreateDirectory creates a file
func storeCreateDirectory(path string) error {
return os.Mkdir(path, os.ModePerm)
}
// storeWriteFile writes a file with the given content
func storeWriteFile(file, content string) error {
return ioutil.WriteFile(file, []byte(content), os.ModePerm)
}
// storeReadFile returns the content of the given file
func storeReadFile(file string) (content string, err error) {
cnt, err := ioutil.ReadFile(file)
content = string(cnt)
return