Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Jonny Schäfer
tubus
Commits
01dc0aef
Commit
01dc0aef
authored
Dec 09, 2015
by
Jonny Schäfer
Browse files
Initial Commit
parents
Pipeline
#171
skipped
Changes
9
Pipelines
1
Show whitespace changes
Inline
Side-by-side
.gitignore
0 → 100644
View file @
01dc0aef
/tubus
cli.go
0 → 100644
View file @
01dc0aef
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
(
"
\x1b
c"
)
}
// 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
}
error.go
0 → 100644
View file @
01dc0aef
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\n
I am Sorry :("
)
}
}
logic.go
0 → 100644
View file @
01dc0aef
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
()
}
readline.go
0 → 100644
View file @
01dc0aef
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?"
)
}
store.go
0 → 100644
View file @
01dc0aef
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
{