diff --git a/.github/workflows/build-arm-raspberry-pi.yml.bak b/.github/workflows/build-arm-raspberry-pi.yml.bak new file mode 100644 index 00000000..62c095fd --- /dev/null +++ b/.github/workflows/build-arm-raspberry-pi.yml.bak @@ -0,0 +1,35 @@ +name: Build for Raspberry Pi +on: [push] + +jobs: + build: + runs-on: self-hosted + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v2 + + - name: Set env + run: if grep -Fxq "devel" cmd/go2tv/version.txt;then echo "GO2TV_VERSION=$(cat cmd/go2tv/version.txt)";else echo "GO2TV_VERSION=v$(cat cmd/go2tv/version.txt)";fi >> $GITHUB_ENV + + - name: Set up Go + run: | + wget -nv https://go.dev/dl/go1.17.5.linux-armv6l.tar.gz + sudo tar xzf go1.17.5.linux-armv6l.tar.gz -C /usr/local/ + rm go1.17.5.linux-armv6l.tar.gz + + - name: Get dependencies + run: sudo apt update && sudo apt install -y xorg-dev + + - name: Package (Raspberry Pi) + run: PATH=$PATH:/usr/local/go/bin go build -ldflags "-s -w" -o go2tv cmd/go2tv/go2tv.go + + - uses: actions/upload-artifact@v2 + with: + name: go2tv_${{ env.GO2TV_VERSION }}_linux_arm + path: | + LICENSE + README.md + go2tv + retention-days: 2 diff --git a/.github/workflows/build-arm.yml b/.github/workflows/build-arm.yml index be2db476..69d110cc 100644 --- a/.github/workflows/build-arm.yml +++ b/.github/workflows/build-arm.yml @@ -30,9 +30,9 @@ jobs: - uses: actions/upload-artifact@v2 with: - name: go2tv_${{ env.GO2TV_VERSION }}_linux_armv6 + name: go2tv_${{ env.GO2TV_VERSION }}_linux_arm path: | LICENSE README.md go2tv - retention-days: 2 \ No newline at end of file + retention-days: 2 diff --git a/README.md b/README.md index ebfa3713..9efce79a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Cast your media files to UPnP/DLNA Media Renderers and Smart TVs. --- GUI mode ----- -![](https://i.imgur.com/pD5P6Fz.png) -![](https://i.imgur.com/karpZgp.png) +![](https://i.imgur.com/Eq3UkuD.png) +![](https://i.imgur.com/B7wF14V.png) CLI mode ----- diff --git a/cmd/go2tv/go2tv.go b/cmd/go2tv/go2tv.go index 841eb249..5aa38e36 100644 --- a/cmd/go2tv/go2tv.go +++ b/cmd/go2tv/go2tv.go @@ -26,7 +26,6 @@ import ( var ( //go:embed version.txt version string - dmrURL string mediaArg = flag.String("v", "", "Local path to the video/audio file. (Triggers the CLI mode)") urlArg = flag.String("u", "", "HTTP URL to the media file. URL streaming does not support seek operations. (Triggers the CLI mode)") subsArg = flag.String("s", "", "Local path to the subtitles file.") @@ -35,17 +34,24 @@ var ( versionPtr = flag.Bool("version", false, "Print version.") ) +type flagResults struct { + dmrURL string + exit bool +} + func main() { guiEnabled := true var mediaFile interface{} flag.Parse() - exit, err := checkflags() + flagRes, err := processflags() check(err) - if exit { + + if flagRes.exit { os.Exit(0) } - if *mediaArg != "" || *urlArg != "" { + + if len(os.Args) > 1 { guiEnabled = false } @@ -74,7 +80,6 @@ func main() { mediaType, err = utils.GetMimeDetailsFromFile(absMediaFile) check(err) - case io.ReadCloser: absMediaFile = *urlArg } @@ -82,10 +87,10 @@ func main() { absSubtitlesFile, err := filepath.Abs(*subsArg) check(err) - upnpServicesURLs, err := soapcalls.DMRextractor(dmrURL) + upnpServicesURLs, err := soapcalls.DMRextractor(flagRes.dmrURL) check(err) - whereToListen, err := utils.URLtoListenIPandPort(dmrURL) + whereToListen, err := utils.URLtoListenIPandPort(flagRes.dmrURL) check(err) scr, err := interactive.InitTcellNewScreen() @@ -117,9 +122,6 @@ func main() { // Wait for HTTP server to properly initialize <-serverStarted - err = tvdata.SendtoTV("Play1") - check(err) - scr.InterInit(tvdata) } @@ -131,15 +133,26 @@ func check(err error) { } func listFlagFunction() error { - if len(devices.Devices) == 0 { - return errors.New("-l and -t can't be used together") + flagsEnabled := 0 + flag.Visit(func(f *flag.Flag) { + flagsEnabled++ + }) + + if flagsEnabled > 1 { + return errors.New("cant combine -l with other flags") } + + deviceList, err := devices.LoadSSDPservices(1) + if err != nil { + return errors.New("failed to list devices") + } + fmt.Println() // We loop through this map twice as we need to maintain // the correct order. keys := make([]string, 0) - for k := range devices.Devices { + for k := range deviceList { keys = append(keys, k) } @@ -156,42 +169,48 @@ func listFlagFunction() error { fmt.Printf("%sDevice %v%s\n", boldStart, q+1, boldEnd) fmt.Printf("%s--------%s\n", boldStart, boldEnd) fmt.Printf("%sModel:%s %s\n", boldStart, boldEnd, k) - fmt.Printf("%sURL:%s %s\n", boldStart, boldEnd, devices.Devices[k]) + fmt.Printf("%sURL:%s %s\n", boldStart, boldEnd, deviceList[k]) fmt.Println() } return nil } -func checkflags() (bool, error) { +func processflags() (*flagResults, error) { checkVerflag() + res := &flagResults{ + exit: false, + dmrURL: "", + } + if checkGUI() { - return false, nil + return res, nil } - if err := checkTflag(); err != nil { - return false, fmt.Errorf("checkflags error: %w", err) + if err := checkTflag(res); err != nil { + return nil, fmt.Errorf("checkflags error: %w", err) } list, err := checkLflag() if err != nil { - return false, fmt.Errorf("checkflags error: %w", err) + return nil, fmt.Errorf("checkflags error: %w", err) } - if err := checkVflag(); err != nil { - return false, fmt.Errorf("checkflags error: %w", err) + if list { + res.exit = true + return res, nil } - if list { - return true, nil + if err := checkVflag(); err != nil { + return nil, fmt.Errorf("checkflags error: %w", err) } if err := checkSflag(); err != nil { - return false, fmt.Errorf("checkflags error: %w", err) + return nil, fmt.Errorf("checkflags error: %w", err) } - return false, nil + return res, nil } func checkVflag() error { @@ -222,24 +241,25 @@ func checkSflag() error { return nil } -func checkTflag() error { - if *targetPtr == "" { - err := devices.LoadSSDPservices(1) +func checkTflag(res *flagResults) error { + if *targetPtr != "" { + // Validate URL before proceeding. + _, err := url.ParseRequestURI(*targetPtr) if err != nil { - return fmt.Errorf("checkTflag service loading error: %w", err) + return fmt.Errorf("checkTflag parse error: %w", err) } - dmrURL, err = devices.DevicePicker(1) + res.dmrURL = *targetPtr + } else { + deviceList, err := devices.LoadSSDPservices(1) if err != nil { - return fmt.Errorf("checkTflag device picker error: %w", err) + return fmt.Errorf("checkTflag service loading error: %w", err) } - } else { - // Validate URL before proceeding. - _, err := url.ParseRequestURI(*targetPtr) + + res.dmrURL, err = devices.DevicePicker(deviceList, 1) if err != nil { - return fmt.Errorf("checkTflag parse error: %w", err) + return fmt.Errorf("checkTflag device picker error: %w", err) } - dmrURL = *targetPtr } return nil @@ -257,7 +277,7 @@ func checkLflag() (bool, error) { } func checkVerflag() { - if *versionPtr { + if *versionPtr && os.Args[1] == "-version" { fmt.Printf("Go2TV Version: %s\n", version) os.Exit(0) } diff --git a/cmd/go2tv/version.txt b/cmd/go2tv/version.txt index ee672d89..ed21137e 100644 --- a/cmd/go2tv/version.txt +++ b/cmd/go2tv/version.txt @@ -1 +1 @@ -1.9.1 \ No newline at end of file +1.10.0 \ No newline at end of file diff --git a/go.mod b/go.mod index 44c51218..6a740cac 100644 --- a/go.mod +++ b/go.mod @@ -6,23 +6,24 @@ require ( fyne.io/fyne/v2 v2.1.2 github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gdamore/tcell/v2 v2.4.0 - github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 // indirect + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect github.com/godbus/dbus/v5 v5.0.6 // indirect - github.com/h2non/filetype v1.1.1 + github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect + github.com/h2non/filetype v1.1.3 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 github.com/koron/go-ssdp v0.0.2 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.13 github.com/pkg/errors v0.9.1 - github.com/srwiley/oksvg v0.0.0-20211104221756-aeb4ca2c1505 // indirect - github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 + github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 // indirect + github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 // indirect github.com/stretchr/testify v1.7.0 // indirect - github.com/yuin/goldmark v1.4.4 // indirect - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect - golang.org/x/net v0.0.0-20211116231205-47ca1ff31462 // indirect - golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect + github.com/yuin/goldmark v1.4.6 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 20b379a7..80d33628 100644 --- a/go.sum +++ b/go.sum @@ -18,18 +18,20 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= -github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 h1:1Zi56D0LNfvkzM+BdoxKryvUEdyWO7LP8oRT+oSYJW0= -github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= -github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= -github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c h1:JGCm/+tJ9gC6THUxooTldS+CUDsba0qvkvU3DHklqW8= +github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= @@ -64,14 +66,17 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= -github.com/srwiley/oksvg v0.0.0-20211104221756-aeb4ca2c1505 h1:fWb9FJAdmiORr+NEeaadjEXp6C2qGAjdd7icfppTskE= -github.com/srwiley/oksvg v0.0.0-20211104221756-aeb4ca2c1505/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 h1:XPYXKIuH/n5zpUoEWk2jWV/SjEMNYmqDYmTgbjmhtaI= +github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= -github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 h1:oDMiXaTMyBEuZMU53atpxqYsSB3U1CHkeAu2zr6wTeY= github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= +github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 h1:YR16ysw3I1bqwtEcYV9dpvhHEe7j55hIClkLoAqY31I= +github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -80,8 +85,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= -github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.6 h1:EQ1OkiNq/eMbQxs/2O/A8VDIHERXGH14s19ednd4XIw= +github.com/yuin/goldmark v1.4.6/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -93,8 +98,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211116231205-47ca1ff31462 h1:2vmJlzGKvQ7e/X9XT0XydeWDxmqx8DnegiIMRT+5ssI= -golang.org/x/net v0.0.0-20211116231205-47ca1ff31462/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -108,8 +114,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= @@ -119,6 +126,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/internal/devices/devices.go b/internal/devices/devices.go index 030ed0e0..bcbbab1c 100644 --- a/internal/devices/devices.go +++ b/internal/devices/devices.go @@ -9,18 +9,13 @@ import ( "github.com/pkg/errors" ) -var ( - // Devices map to maintain a list of all the discovered devices. - Devices = make(map[string]string) -) - // LoadSSDPservices . -func LoadSSDPservices(delay int) error { +func LoadSSDPservices(delay int) (map[string]string, error) { // Reset device list every time we call this. - Devices = make(map[string]string) + deviceList := make(map[string]string) list, err := ssdp.Search(ssdp.All, delay, "") if err != nil { - return fmt.Errorf("LoadSSDPservices search error: %w", err) + return nil, fmt.Errorf("LoadSSDPservices search error: %w", err) } for _, srv := range list { @@ -30,34 +25,36 @@ func LoadSSDPservices(delay int) error { if srv.Type == "urn:schemas-upnp-org:service:AVTransport:1" { friendlyName, err := soapcalls.GetFriendlyName(srv.Location) if err != nil { - friendlyName = srv.Server + continue } - Devices[friendlyName] = srv.Location + deviceList[friendlyName] = srv.Location } } - if len(Devices) > 0 { - return nil + if len(deviceList) > 0 { + return deviceList, nil } - return errors.New("loadSSDPservices: No available Media Renderers") + return nil, errors.New("loadSSDPservices: No available Media Renderers") } // DevicePicker . -func DevicePicker(i int) (string, error) { - if i > len(Devices) || len(Devices) == 0 || i <= 0 { +func DevicePicker(devices map[string]string, i int) (string, error) { + if i > len(devices) || len(devices) == 0 || i <= 0 { return "", errors.New("devicePicker: Requested device not available") } keys := make([]string, 0) - for k := range Devices { + for k := range devices { keys = append(keys, k) } + sort.Strings(keys) + for q, k := range keys { if i == q+1 { - return Devices[k], nil + return devices[k], nil } } return "", errors.New("devicePicker: Something went terribly wrong") diff --git a/internal/gui/actions.go b/internal/gui/actions.go index f86ef2ab..eabc93aa 100644 --- a/internal/gui/actions.go +++ b/internal/gui/actions.go @@ -6,11 +6,15 @@ package gui import ( "context" "fmt" + "io" "path/filepath" "sort" + "strconv" + "strings" "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/theme" @@ -20,6 +24,7 @@ import ( "github.com/alexballas/go2tv/internal/urlstreamer" "github.com/alexballas/go2tv/internal/utils" "github.com/pkg/errors" + "github.com/skratchdot/open-golang/open" ) func muteAction(screen *NewScreen) { @@ -153,6 +158,8 @@ func subsAction(screen *NewScreen) { func playAction(screen *NewScreen) { var mediaFile interface{} + screen.PlayPause.Disable() + w := screen.Current currentState := screen.getScreenState() @@ -190,10 +197,6 @@ func playAction(screen *NewScreen) { return } - if screen.tvdata == nil { - stopAction(screen) - } - whereToListen, err := utils.URLtoListenIPandPort(screen.controlURL) check(w, err) if err != nil { @@ -223,7 +226,7 @@ func playAction(screen *NewScreen) { if screen.ExternalMediaURL.Checked { // We need to define the screen.mediafile // as this is the core item in our structure - // that define that something is being streammed. + // that defines that something is being streamed. // We use its value for many checks in our code. screen.mediafile = screen.MediaText.Text @@ -232,12 +235,37 @@ func playAction(screen *NewScreen) { // the io.Copy operation to fail with "broken pipe". // That's good enough for us since right after that // we close the io.ReadCloser. - mediaFile, err = urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) + mediaURL, err := urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) check(screen.Current, err) if err != nil { screen.PlayPause.Enable() return } + + mediaURLinfo, err := urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) + check(screen.Current, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo) + check(w, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaFile = mediaURL + if strings.Contains(mediaType, "image") { + readerToBytes, err := io.ReadAll(mediaURL) + mediaURL.Close() + if err != nil { + screen.PlayPause.Enable() + return + } + mediaFile = readerToBytes + } } screen.tvdata = &soapcalls.TVPayload{ @@ -298,6 +326,33 @@ func clearsubsAction(screen *NewScreen) { screen.SubsText.Refresh() } +func previewmedia(screen *NewScreen) { + w := screen.Current + + if screen.mediafile == "" { + check(w, errors.New("please select a media file")) + return + } + + mediaType, err := utils.GetMimeDetailsFromFile(screen.mediafile) + check(w, err) + + mediaTypeSlice := strings.Split(mediaType, "/") + switch mediaTypeSlice[0] { + case "image": + img := canvas.NewImageFromFile(screen.mediafile) + img.FillMode = 1 + imgw := fyne.CurrentApp().NewWindow(filepath.Base(screen.mediafile)) + imgw.SetContent(img) + imgw.Resize(fyne.NewSize(800, 600)) + imgw.CenterOnScreen() + imgw.Show() + default: + err := open.Run(screen.mediafile) + check(w, err) + } +} + func stopAction(screen *NewScreen) { w := screen.Current @@ -325,13 +380,14 @@ func stopAction(screen *NewScreen) { } func getDevices(delay int) ([]devType, error) { - if err := devices.LoadSSDPservices(delay); err != nil { + deviceList, err := devices.LoadSSDPservices(delay) + if err != nil { return nil, fmt.Errorf("getDevices error: %w", err) } // We loop through this map twice as we need to maintain // the correct order. keys := make([]string, 0) - for k := range devices.Devices { + for k := range deviceList { keys = append(keys, k) } @@ -339,8 +395,46 @@ func getDevices(delay int) ([]devType, error) { guiDeviceList := make([]devType, 0) for _, k := range keys { - guiDeviceList = append(guiDeviceList, devType{k, devices.Devices[k]}) + guiDeviceList = append(guiDeviceList, devType{k, deviceList[k]}) } return guiDeviceList, nil } + +func volumeAction(screen *NewScreen, up bool) { + w := screen.Current + if screen.renderingControlURL == "" { + check(w, errors.New("please select a device")) + return + } + + if screen.tvdata == nil { + // If tvdata is nil, we just need to set RenderingControlURL if we want + // to control the sound. We should still rely on the play action to properly + // populate our tvdata type. + screen.tvdata = &soapcalls.TVPayload{RenderingControlURL: screen.renderingControlURL} + } + + currentVolume, err := screen.tvdata.GetVolumeSoapCall() + if err != nil { + check(w, errors.New("could not get the volume levels")) + return + } + + setVolume := currentVolume - 1 + + if up { + setVolume = currentVolume + 1 + } + + if setVolume < 0 { + setVolume = 0 + } + + stringVolume := strconv.Itoa(setVolume) + + if err := screen.tvdata.SetVolumeSoapCall(stringVolume); err != nil { + check(w, errors.New("could not send volume action")) + } + +} diff --git a/internal/gui/actions_mobile.go b/internal/gui/actions_mobile.go index 46cfe0be..2fdcea93 100644 --- a/internal/gui/actions_mobile.go +++ b/internal/gui/actions_mobile.go @@ -6,7 +6,10 @@ package gui import ( "context" "fmt" + "io" "sort" + "strconv" + "strings" "time" "fyne.io/fyne/v2" @@ -124,6 +127,8 @@ func playAction(screen *NewScreen) { var mediaFile, subsFile interface{} w := screen.Current + screen.PlayPause.Disable() + currentState := screen.getScreenState() if currentState == "Paused" { @@ -159,10 +164,6 @@ func playAction(screen *NewScreen) { return } - if screen.tvdata == nil { - stopAction(screen) - } - whereToListen, err := utils.URLtoListenIPandPort(screen.controlURL) check(w, err) if err != nil { @@ -178,11 +179,38 @@ func playAction(screen *NewScreen) { return } - mediaFile, err = storage.OpenFileFromURI(screen.mediafile) - check(screen.Current, err) - if err != nil { - screen.PlayPause.Enable() - return + if screen.mediafile != nil { + mediaURL, err := storage.OpenFileFromURI(screen.mediafile) + check(screen.Current, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaURLinfo, err := storage.OpenFileFromURI(screen.mediafile) + check(screen.Current, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo) + check(w, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaFile = mediaURL + if strings.Contains(mediaType, "image") { + readerToBytes, err := io.ReadAll(mediaURL) + mediaURL.Close() + if err != nil { + screen.PlayPause.Enable() + return + } + mediaFile = readerToBytes + } } if screen.subsfile != nil { @@ -200,12 +228,37 @@ func playAction(screen *NewScreen) { // the io.Copy operation to fail with "broken pipe". // That's good enough for us since right after that // we close the io.ReadCloser. - mediaFile, err = urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) + mediaURL, err := urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) check(screen.Current, err) if err != nil { screen.PlayPause.Enable() return } + + mediaURLinfo, err := urlstreamer.StreamURL(context.Background(), screen.MediaText.Text) + check(screen.Current, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo) + check(w, err) + if err != nil { + screen.PlayPause.Enable() + return + } + + mediaFile = mediaURL + if strings.Contains(mediaType, "image") { + readerToBytes, err := io.ReadAll(mediaURL) + mediaURL.Close() + if err != nil { + screen.PlayPause.Enable() + return + } + mediaFile = readerToBytes + } } screen.tvdata = &soapcalls.TVPayload{ @@ -293,13 +346,14 @@ func stopAction(screen *NewScreen) { } func getDevices(delay int) ([]devType, error) { - if err := devices.LoadSSDPservices(delay); err != nil { + deviceList, err := devices.LoadSSDPservices(delay) + if err != nil { return nil, fmt.Errorf("getDevices error: %w", err) } // We loop through this map twice as we need to maintain // the correct order. keys := make([]string, 0) - for k := range devices.Devices { + for k := range deviceList { keys = append(keys, k) } @@ -307,8 +361,46 @@ func getDevices(delay int) ([]devType, error) { guiDeviceList := make([]devType, 0) for _, k := range keys { - guiDeviceList = append(guiDeviceList, devType{k, devices.Devices[k]}) + guiDeviceList = append(guiDeviceList, devType{k, deviceList[k]}) } return guiDeviceList, nil } + +func volumeAction(screen *NewScreen, up bool) { + w := screen.Current + if screen.renderingControlURL == "" { + check(w, errors.New("please select a device")) + return + } + + if screen.tvdata == nil { + // If tvdata is nil, we just need to set RenderingControlURL if we want + // to control the sound. We should still rely on the play action to properly + // populate our tvdata type. + screen.tvdata = &soapcalls.TVPayload{RenderingControlURL: screen.renderingControlURL} + } + + currentVolume, err := screen.tvdata.GetVolumeSoapCall() + if err != nil { + check(w, errors.New("could not get the volume levels")) + return + } + + setVolume := currentVolume - 1 + + if up { + setVolume = currentVolume + 1 + } + + if setVolume < 0 { + setVolume = 0 + } + + stringVolume := strconv.Itoa(setVolume) + + if err := screen.tvdata.SetVolumeSoapCall(stringVolume); err != nil { + check(w, errors.New("could not send volume action")) + } + +} diff --git a/internal/gui/gui.go b/internal/gui/gui.go index 7f1bf07f..e30d35f3 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -64,9 +64,11 @@ func Start(s *NewScreen) { container.NewTabItem("Go2TV", container.NewPadded(mainWindow(s))), container.NewTabItem("About", aboutWindow(s)), ) + w.SetContent(tabs) w.Resize(fyne.NewSize(w.Canvas().Size().Width*1.2, w.Canvas().Size().Height*1.3)) w.CenterOnScreen() + w.SetMaster() w.ShowAndRun() os.Exit(0) } @@ -114,7 +116,7 @@ func InitFyneNewScreen(v string) *NewScreen { return &NewScreen{ Current: w, currentmfolder: currentdir, - mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".mp3", ".flac", ".wav"}, + mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".mp3", ".flac", ".wav", ".jpg", ".jpeg", ".png"}, version: v, } } diff --git a/internal/gui/layouts.go b/internal/gui/layouts.go index 2325411a..1119a83c 100644 --- a/internal/gui/layouts.go +++ b/internal/gui/layouts.go @@ -1,6 +1,8 @@ package gui -import "fyne.io/fyne/v2" +import ( + "fyne.io/fyne/v2" +) func (d *mainButtonsLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { w, h := float32(0), float32(0) @@ -17,8 +19,7 @@ func (d *mainButtonsLayout) Layout(objects []fyne.CanvasObject, containerSize fy bigButtonSize := containerSize.Width for q, o := range objects { - z := q + 1 - if z%2 == 0 { + if q != 0 && q != len(objects)-1 { bigButtonSize = bigButtonSize - o.MinSize().Width } } @@ -26,8 +27,8 @@ func (d *mainButtonsLayout) Layout(objects []fyne.CanvasObject, containerSize fy for q, o := range objects { var size fyne.Size - switch q % 2 { - case 0: + switch q { + case 0, len(objects) - 1: size = fyne.NewSize(bigButtonSize, o.MinSize().Height) default: size = o.MinSize() diff --git a/internal/gui/main.go b/internal/gui/main.go index 5efc679f..59e27551 100644 --- a/internal/gui/main.go +++ b/internal/gui/main.go @@ -14,9 +14,9 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/alexballas/go2tv/internal/devices" "github.com/alexballas/go2tv/internal/soapcalls" "github.com/alexballas/go2tv/internal/utils" + "golang.org/x/time/rate" ) func mainWindow(s *NewScreen) fyne.CanvasObject { @@ -69,9 +69,7 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { sfile.Disable() sfiletext.Disable() - var playpause *widget.Button - playpause = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { - playpause.Disable() + playpause := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { go playAction(s) }) @@ -79,12 +77,16 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { go stopAction(s) }) + volumeup := widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { + go volumeAction(s, true) + }) + muteunmute := widget.NewButtonWithIcon("", theme.VolumeMuteIcon(), func() { go muteAction(s) }) - unmute := widget.NewButtonWithIcon("", theme.VolumeUpIcon(), func() { - go unmuteAction(s) + volumedown := widget.NewButtonWithIcon("", theme.ContentRemoveIcon(), func() { + go volumeAction(s, false) }) clearmedia := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { @@ -95,6 +97,19 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { go clearsubsAction(s) }) + // previewmedia spawns external applications. + // Since there is no way to monitor the time it takes + // for the apps to load, we introduce a rate limit + // for the specific action. + throttle := rate.Every(3 * time.Second) + r := rate.NewLimiter(throttle, 1) + previewmedia := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func() { + if !r.Allow() { + return + } + go previewmedia(s) + }) + sfilecheck := widget.NewCheck("Custom Subtitles", func(b bool) {}) externalmedia := widget.NewCheck("Media from URL", func(b bool) {}) medialoop := widget.NewCheck("Loop Selected", func(b bool) {}) @@ -104,8 +119,6 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { subsfilelabel := canvas.NewText("Subtitles:", nil) devicelabel := canvas.NewText("Select Device:", nil) - unmute.Hide() - list = widget.NewList( func() int { return len(data) @@ -126,14 +139,16 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { s.SubsText = sfiletext s.DeviceList = list - playpausemutestop := container.New(&mainButtonsLayout{}, playpause, muteunmute, stop) + actionbuttons := container.New(&mainButtonsLayout{}, playpause, volumedown, muteunmute, volumeup, stop) + + mrightbuttons := container.NewHBox(previewmedia, clearmedia) checklists := container.NewHBox(externalmedia, sfilecheck, medialoop, nextmedia) mediasubsbuttons := container.New(layout.NewGridLayout(2), mfile, sfile) + mfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, mrightbuttons), mrightbuttons, mfiletext) sfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearsubs), clearsubs, sfiletext) - mfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearmedia), clearmedia, mfiletext) viewfilescont := container.New(layout.NewFormLayout(), mediafilelabel, mfiletextArea, subsfilelabel, sfiletextArea) - buttons := container.NewVBox(mediasubsbuttons, viewfilescont, checklists, playpausemutestop, container.NewPadded(devicelabel)) + buttons := container.NewVBox(mediasubsbuttons, viewfilescont, checklists, actionbuttons, container.NewPadded(devicelabel)) content := container.New(layout.NewBorderLayout(buttons, nil, nil, nil), buttons, list) // Widgets actions @@ -163,6 +178,7 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { nextmedia.SetChecked(false) nextmedia.Disable() mfile.Disable() + previewmedia.Disable() // rename the label mediafilelabel.Text = "URL:" @@ -179,6 +195,7 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { medialoop.Enable() nextmedia.Enable() mfile.Enable() + previewmedia.Enable() mediafilelabel.Text = "File:" mfiletext.SetPlaceHolder("") mfiletext.Text = "" @@ -206,10 +223,10 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { func refreshDevList(s *NewScreen, data *[]devType) { refreshDevices := time.NewTicker(5 * time.Second) - for range refreshDevices.C { - oldListSize := len(devices.Devices) + for range refreshDevices.C { datanew, _ := getDevices(2) + oldListSize := len(*data) // check to see if the new refresh includes // one of the already selected devices diff --git a/internal/gui/main_mobile.go b/internal/gui/main_mobile.go index 1b24b554..ece70a1c 100644 --- a/internal/gui/main_mobile.go +++ b/internal/gui/main_mobile.go @@ -14,7 +14,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/alexballas/go2tv/internal/devices" "github.com/alexballas/go2tv/internal/soapcalls" "github.com/alexballas/go2tv/internal/utils" ) @@ -68,9 +67,7 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { sfiletext.Disable() - var playpause *widget.Button - playpause = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { - playpause.Disable() + playpause := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { go playAction(s) }) @@ -78,18 +75,22 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { go stopAction(s) }) + volumeup := widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { + go volumeAction(s, true) + }) + muteunmute := widget.NewButtonWithIcon("", theme.VolumeMuteIcon(), func() { go muteAction(s) }) - unmute := widget.NewButtonWithIcon("", theme.VolumeUpIcon(), func() { - go unmuteAction(s) + volumedown := widget.NewButtonWithIcon("", theme.ContentRemoveIcon(), func() { + go volumeAction(s, false) }) clearmedia := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { go clearmediaAction(s) }) - + clearsubs := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { go clearsubsAction(s) }) @@ -101,8 +102,6 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { subsfilelabel := canvas.NewText("Subtitles:", nil) devicelabel := canvas.NewText("Select Device:", nil) - unmute.Hide() - list = widget.NewList( func() int { return len(data) @@ -122,14 +121,14 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { s.SubsText = sfiletext s.DeviceList = list - playpausemutestop := container.New(&mainButtonsLayout{}, playpause, muteunmute, stop) + actionbuttons := container.New(&mainButtonsLayout{}, playpause, volumedown, muteunmute, volumeup, stop) checklists := container.NewHBox(externalmedia, medialoop) mediasubsbuttons := container.New(layout.NewGridLayout(2), mfile, sfile) sfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearsubs), clearsubs, sfiletext) mfiletextArea := container.New(layout.NewBorderLayout(nil, nil, nil, clearmedia), clearmedia, mfiletext) viewfilescont := container.New(layout.NewFormLayout(), mediafilelabel, mfiletextArea, subsfilelabel, sfiletextArea) - buttons := container.NewVBox(mediasubsbuttons, viewfilescont, checklists, playpausemutestop, container.NewPadded(devicelabel)) + buttons := container.NewVBox(mediasubsbuttons, viewfilescont, checklists, actionbuttons, container.NewPadded(devicelabel)) content := container.New(layout.NewBorderLayout(buttons, nil, nil, nil), buttons, list) // Widgets actions @@ -187,10 +186,10 @@ func mainWindow(s *NewScreen) fyne.CanvasObject { func refreshDevList(s *NewScreen, data *[]devType) { refreshDevices := time.NewTicker(5 * time.Second) - for range refreshDevices.C { - oldListSize := len(devices.Devices) + for range refreshDevices.C { datanew, _ := getDevices(2) + oldListSize := len(*data) // check to see if the new refresh includes // one of the already selected devices diff --git a/internal/httphandlers/httphandlers.go b/internal/httphandlers/httphandlers.go index 08209367..70e9089b 100644 --- a/internal/httphandlers/httphandlers.go +++ b/internal/httphandlers/httphandlers.go @@ -1,6 +1,7 @@ package httphandlers import ( + "bytes" "fmt" "html" "io" @@ -9,6 +10,7 @@ import ( "net/url" "os" "strings" + "time" "github.com/alexballas/go2tv/internal/soapcalls" "github.com/alexballas/go2tv/internal/utils" @@ -55,7 +57,7 @@ func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, media, subtitles return fmt.Errorf("failed to parse CallbackURL: %w", err) } - s.mux.HandleFunc(mURL.Path, s.serveMediaHandler(media)) + s.mux.HandleFunc(mURL.Path, s.serveMediaHandler(tvpayload, media)) s.mux.HandleFunc(sURL.Path, s.serveSubtitlesHandler(subtitles)) s.mux.HandleFunc(callbackURL.Path, s.callbackHandler(tvpayload, screen)) @@ -70,15 +72,15 @@ func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, media, subtitles return nil } -func (s *HTTPserver) serveMediaHandler(media interface{}) http.HandlerFunc { +func (s *HTTPserver) serveMediaHandler(tv *soapcalls.TVPayload, media interface{}) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - serveContent(w, req, media, true) + serveContent(w, req, tv, media, true) } } func (s *HTTPserver) serveSubtitlesHandler(subs interface{}) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - serveContent(w, req, subs, false) + serveContent(w, req, nil, subs, false) } } @@ -155,7 +157,7 @@ func NewServer(a string) *HTTPserver { return &srv } -func serveContent(w http.ResponseWriter, r *http.Request, s interface{}, isMedia bool) { +func serveContent(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayload, s interface{}, isMedia bool) { respHeader := w.Header() if isMedia { respHeader["transferMode.dlna.org"] = []string{"Streaming"} @@ -164,14 +166,20 @@ func serveContent(w http.ResponseWriter, r *http.Request, s interface{}, isMedia respHeader["transferMode.dlna.org"] = []string{"Interactive"} } + var mediaType string + if tv != nil { + mediaType = tv.MediaType + } + switch f := s.(type) { case string: if r.Header.Get("getcontentFeatures.dlna.org") == "1" { - contentFeatures, err := utils.BuildContentFeatures(f, "01", false) + contentFeatures, err := utils.BuildContentFeatures(mediaType, "01", false) if err != nil { http.NotFound(w, r) return } + respHeader["contentFeatures.dlna.org"] = []string{contentFeatures} } @@ -191,9 +199,30 @@ func serveContent(w http.ResponseWriter, r *http.Request, s interface{}, isMedia name := strings.TrimLeft(r.URL.Path, "/") http.ServeContent(w, r, name, fileStat.ModTime(), filePath) + case []byte: + if r.Header.Get("getcontentFeatures.dlna.org") == "1" { + contentFeatures, err := utils.BuildContentFeatures(mediaType, "01", false) + if err != nil { + http.NotFound(w, r) + return + } + + respHeader["contentFeatures.dlna.org"] = []string{contentFeatures} + } + + bReader := bytes.NewReader(f) + + name := strings.TrimLeft(r.URL.Path, "/") + http.ServeContent(w, r, name, time.Now(), bReader) + case io.ReadCloser: if r.Header.Get("getcontentFeatures.dlna.org") == "1" { - contentFeatures, _ := utils.BuildContentFeatures("", "00", false) + contentFeatures, err := utils.BuildContentFeatures(mediaType, "00", false) + if err != nil { + http.NotFound(w, r) + return + } + respHeader["contentFeatures.dlna.org"] = []string{contentFeatures} } diff --git a/internal/httphandlers/httphandlers_test.go b/internal/httphandlers/httphandlers_test.go index 2b39c9d7..48b2bda0 100644 --- a/internal/httphandlers/httphandlers_test.go +++ b/internal/httphandlers/httphandlers_test.go @@ -29,7 +29,7 @@ func TestServeContent(t *testing.T) { r.Header.Add("getcontentFeatures.dlna.org", "1") - serveContent(w, r, tc.input, false) + serveContent(w, r, nil, tc.input, false) if w.Result().StatusCode != http.StatusOK { t.Errorf("%s: got: %s.", tc.name, w.Result().Status) diff --git a/internal/interactive/interactive.go b/internal/interactive/interactive.go index a19a7fbf..a9182e80 100644 --- a/internal/interactive/interactive.go +++ b/internal/interactive/interactive.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "os" + "strconv" "strings" "sync" "time" @@ -82,8 +83,9 @@ func (p *NewScreen) EmitMsg(inputtext string) { } else { p.emitStr(w/2-len("MUTED")/2, h/2+2, blinkStyle, "MUTED") } - p.emitStr(w/2-len(`Press "p" to Pause/Play, "m" to Mute/Unmute`)/2, h/2+4, tcell.StyleDefault, `Press "p" to Pause/Play, "m" to Mute/Unmute`) - + p.emitStr(w/2-len(`"p" (Play/Pause)`)/2, h/2+4, tcell.StyleDefault, `"p" (Play/Pause)`) + p.emitStr(w/2-len(`"m" (Mute/Unmute)`)/2, h/2+6, tcell.StyleDefault, `"m" (Mute/Unmute)`) + p.emitStr(w/2-len(`"Page Up" "Page Down" (Volume Up/Down)`)/2, h/2+8, tcell.StyleDefault, `"Page Up" "Page Down" (Volume Up/Down)`) s.Show() } @@ -109,8 +111,8 @@ func (p *NewScreen) InterInit(tv *soapcalls.TVPayload) { encoding.Register() s := p.Current - if e := s.Init(); e != nil { - _, _ = fmt.Fprintf(os.Stderr, "%v\n", e) + if err := s.Init(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } @@ -122,6 +124,14 @@ func (p *NewScreen) InterInit(tv *soapcalls.TVPayload) { p.updateLastAction("Waiting for status...") p.EmitMsg(p.getLastAction()) + // Sending the Play1 action sooner may result + // in a panic error since we need to properly + // initialize the tcell window. + if err := tv.SendtoTV("Play1"); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + for { switch ev := s.PollEvent().(type) { case *tcell.EventResize: @@ -142,6 +152,24 @@ func (p *NewScreen) HandleKeyEvent(ev *tcell.EventKey) { p.Fini() } + if ev.Key() == tcell.KeyPgUp || ev.Key() == tcell.KeyPgDn { + currentVolume, err := tv.GetVolumeSoapCall() + if err != nil { + return + } + + setVolume := currentVolume - 1 + if ev.Key() == tcell.KeyPgUp { + setVolume = currentVolume + 1 + } + + stringVolume := strconv.Itoa(setVolume) + + if err := tv.SetVolumeSoapCall(stringVolume); err != nil { + return + } + } + switch ev.Rune() { case 'p': if flipflop { diff --git a/internal/soapcalls/friendlyname.go b/internal/soapcalls/friendlyname.go index 036f95ef..1fee5da0 100644 --- a/internal/soapcalls/friendlyname.go +++ b/internal/soapcalls/friendlyname.go @@ -21,6 +21,8 @@ func GetFriendlyName(dmr string) (string, error) { return "", fmt.Errorf("failed to create NewRequest for GetFriendlyName: %w", err) } + req.Header.Set("Connection", "close") + resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("failed to send HTTP request for GetFriendlyName: %w", err) diff --git a/internal/soapcalls/soapbuilders.go b/internal/soapcalls/soapbuilders.go index 5aa77d40..3bd19d77 100644 --- a/internal/soapcalls/soapbuilders.go +++ b/internal/soapcalls/soapbuilders.go @@ -195,6 +195,51 @@ type GetMuteAction struct { Channel string } +// GetVolumeEnvelope . +type GetVolumeEnvelope struct { + XMLName xml.Name `xml:"s:Envelope"` + Schema string `xml:"xmlns:s,attr"` + Encoding string `xml:"s:encodingStyle,attr"` + GetVolumeBody GetVolumeBody `xml:"s:Body"` +} + +// GetVolumeBody . +type GetVolumeBody struct { + XMLName xml.Name `xml:"s:Body"` + GetVolumeAction GetVolumeAction `xml:"u:GetVolume"` +} + +// GetVolumeAction . +type GetVolumeAction struct { + XMLName xml.Name `xml:"u:GetVolume"` + RenderingControl string `xml:"xmlns:u,attr"` + InstanceID string + Channel string +} + +// SetVolumeEnvelope - As in Play Pause Stop. +type SetVolumeEnvelope struct { + XMLName xml.Name `xml:"s:Envelope"` + Schema string `xml:"xmlns:s,attr"` + Encoding string `xml:"s:encodingStyle,attr"` + SetVolumeBody SetVolumeBody `xml:"s:Body"` +} + +// SetVolumeBody . +type SetVolumeBody struct { + XMLName xml.Name `xml:"s:Body"` + SetVolumeAction SetVolumeAction `xml:"u:SetVolume"` +} + +// SetVolumeAction . +type SetVolumeAction struct { + XMLName xml.Name `xml:"u:SetVolume"` + RenderingControl string `xml:"xmlns:u,attr"` + InstanceID string + Channel string + DesiredVolume string +} + func setAVTransportSoapBuild(mediaURL, mediaType, subtitleURL string) ([]byte, error) { mediaTypeSlice := strings.Split(mediaType, "/") @@ -202,6 +247,8 @@ func setAVTransportSoapBuild(mediaURL, mediaType, subtitleURL string) ([]byte, e switch mediaTypeSlice[0] { case "audio": class = "object.item.audioItem.musicTrack" + case "image": + class = "object.item.imageItem.photo" default: class = "object.item.videoItem.movie" } @@ -413,3 +460,52 @@ func getMuteSoapBuild() ([]byte, error) { return append(xmlStart, b...), nil } + +func getVolumeSoapBuild() ([]byte, error) { + d := GetVolumeEnvelope{ + XMLName: xml.Name{}, + Schema: "http://schemas.xmlsoap.org/soap/envelope/", + Encoding: "http://schemas.xmlsoap.org/soap/encoding/", + GetVolumeBody: GetVolumeBody{ + XMLName: xml.Name{}, + GetVolumeAction: GetVolumeAction{ + XMLName: xml.Name{}, + RenderingControl: "urn:schemas-upnp-org:service:RenderingControl:1", + InstanceID: "0", + Channel: "Master", + }, + }, + } + xmlStart := []byte("") + b, err := xml.Marshal(d) + if err != nil { + return nil, fmt.Errorf("getVolumeSoapBuild Marshal error: %w", err) + } + + return append(xmlStart, b...), nil +} + +func setVolumeSoapBuild(v string) ([]byte, error) { + d := SetVolumeEnvelope{ + XMLName: xml.Name{}, + Schema: "http://schemas.xmlsoap.org/soap/envelope/", + Encoding: "http://schemas.xmlsoap.org/soap/encoding/", + SetVolumeBody: SetVolumeBody{ + XMLName: xml.Name{}, + SetVolumeAction: SetVolumeAction{ + XMLName: xml.Name{}, + RenderingControl: "urn:schemas-upnp-org:service:RenderingControl:1", + InstanceID: "0", + Channel: "Master", + DesiredVolume: v, + }, + }, + } + xmlStart := []byte("") + b, err := xml.Marshal(d) + if err != nil { + return nil, fmt.Errorf("setVolumeSoapBuild Marshal error: %w", err) + } + + return append(xmlStart, b...), nil +} diff --git a/internal/soapcalls/soapbuilders_test.go b/internal/soapcalls/soapbuilders_test.go index 04893a0c..1ed119a1 100644 --- a/internal/soapcalls/soapbuilders_test.go +++ b/internal/soapcalls/soapbuilders_test.go @@ -64,3 +64,27 @@ func TestSetMuteSoapBuild(t *testing.T) { } } } + +func TestGetVolumeSoapBuild(t *testing.T) { + tt := []struct { + name string + want string + }{ + { + `getVolumeSoapBuild Test #1`, + `0Master`, + }, + } + + for _, tc := range tt { + out, err := getVolumeSoapBuild() + if err != nil { + t.Errorf("%s: Failed to call setMuteSoapBuild due to %s", tc.name, err.Error()) + return + } + if string(out) != tc.want { + t.Errorf("%s: got: %s, want: %s.", tc.name, out, tc.want) + return + } + } +} diff --git a/internal/soapcalls/soapcallers.go b/internal/soapcalls/soapcallers.go index cf810890..3ee605b7 100644 --- a/internal/soapcalls/soapcallers.go +++ b/internal/soapcalls/soapcallers.go @@ -41,7 +41,7 @@ type TVPayload struct { MediaType string } -// GetMuteRespBody - Build the Get Mute response body +// GetMuteRespBody - Build the GetMute response body type GetMuteRespBody struct { XMLName xml.Name `xml:"Envelope"` Text string `xml:",chardata"` @@ -57,6 +57,22 @@ type GetMuteRespBody struct { } `xml:"Body"` } +// GetVolumeRespBody - Build the GetVolume response body +type GetVolumeRespBody struct { + XMLName xml.Name `xml:"Envelope"` + Text string `xml:",chardata"` + EncodingStyle string `xml:"encodingStyle,attr"` + S string `xml:"s,attr"` + Body struct { + Text string `xml:",chardata"` + GetVolumeResponse struct { + Text string `xml:",chardata"` + U string `xml:"u,attr"` + CurrentVolume string `xml:"CurrentVolume"` + } `xml:"GetVolumeResponse"` + } `xml:"Body"` +} + func (p *TVPayload) setAVTransportSoapCall() error { parsedURLtransport, err := url.Parse(p.ControlURL) if err != nil { @@ -374,6 +390,92 @@ func (p *TVPayload) SetMuteSoapCall(number string) error { return nil } +// GetVolumeSoapCall - Return volume levels for target device +func (p *TVPayload) GetVolumeSoapCall() (int, error) { + parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL) + if err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall parse error: %w", err) + } + + var xmlbuilder []byte + + xmlbuilder, err = getVolumeSoapBuild() + if err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall build error: %w", err) + } + + client := &http.Client{} + req, err := http.NewRequest("POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder)) + if err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall POST error: %w", err) + } + + req.Header = http.Header{ + "SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#GetVolume"`}, + "content-type": []string{"text/xml"}, + "charset": []string{"utf-8"}, + "Connection": []string{"close"}, + } + + resp, err := client.Do(req) + if err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall Do POST error: %w", err) + } + + defer resp.Body.Close() + + var respGetVolume GetVolumeRespBody + if err = xml.NewDecoder(resp.Body).Decode(&respGetVolume); err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall XML Decode error: %w", err) + } + + intVolume, err := strconv.Atoi(respGetVolume.Body.GetVolumeResponse.CurrentVolume) + if err != nil { + return 0, fmt.Errorf("GetVolumeSoapCall failed to parse volume value: %w", err) + } + + if intVolume < 0 { + intVolume = 0 + } + + return intVolume, nil +} + +// SetVolumeSoapCall - Set the desired volume levels +func (p *TVPayload) SetVolumeSoapCall(v string) error { + parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL) + if err != nil { + return fmt.Errorf("SetMuteSoapCall parse error: %w", err) + } + + var xmlbuilder []byte + + xmlbuilder, err = setVolumeSoapBuild(v) + if err != nil { + return fmt.Errorf("SetMuteSoapCall build error: %w", err) + } + + client := &http.Client{} + req, err := http.NewRequest("POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder)) + if err != nil { + return fmt.Errorf("SetMuteSoapCall POST error: %w", err) + } + + req.Header = http.Header{ + "SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#SetVolume"`}, + "content-type": []string{"text/xml"}, + "charset": []string{"utf-8"}, + "Connection": []string{"close"}, + } + + _, err = client.Do(req) + if err != nil { + return fmt.Errorf("SetMuteSoapCall Do POST error: %w", err) + } + + return nil +} + // SendtoTV - Send to TV. func (p *TVPayload) SendtoTV(action string) error { if action == "Play1" { diff --git a/internal/soapcalls/xmlparsers.go b/internal/soapcalls/xmlparsers.go index 3edf35c5..69243972 100644 --- a/internal/soapcalls/xmlparsers.go +++ b/internal/soapcalls/xmlparsers.go @@ -84,6 +84,8 @@ func DMRextractor(dmrurl string) (*DMRextracted, error) { return nil, fmt.Errorf("DMRextractor GET error: %w", err) } + req.Header.Set("Connection", "close") + xmlresp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("DMRextractor Do GET error: %w", err) diff --git a/internal/utils/dlnatools.go b/internal/utils/dlnatools.go index 46a35462..634aff47 100644 --- a/internal/utils/dlnatools.go +++ b/internal/utils/dlnatools.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "io" "os" "strings" @@ -37,7 +38,9 @@ var ( "video/x-m4v": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", "video/3gpp": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", "video/x-flv": "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", - "audio/mpeg": "DLNA.ORG_PN=MP3"} + "audio/mpeg": "DLNA.ORG_PN=MP3", + "image/jpeg": "JPEG_LRG", + "image/png": "PNG_LRG"} ) func defaultStreamingFlags() string { @@ -49,21 +52,23 @@ func defaultStreamingFlags() string { // BuildContentFeatures - Build the content features string // for the "contentFeatures.dlna.org" header. -func BuildContentFeatures(file string, seek string, transcode bool) (string, error) { +func BuildContentFeatures(mediaType string, seek string, transcode bool) (string, error) { var cf strings.Builder - if file != "" { - ctype, err := GetMimeDetailsFromFile(file) - if err != nil { - return "", fmt.Errorf("BuildContentFeatures error: %w", err) - } - - dlnaProf, profExists := dlnaprofiles[ctype] - if profExists { + if mediaType != "" { + dlnaProf, profExists := dlnaprofiles[mediaType] + switch profExists { + case true: cf.WriteString(dlnaProf + ";") + default: + return "", errors.New("non supported mediaType") } } + // "00" neither time seek range nor range supported + // "01" range supported + // "10" time seek range supported + // "11" both time seek range and range supported switch seek { case "00": cf.WriteString("DLNA.ORG_OP=00;") @@ -108,3 +113,17 @@ func GetMimeDetailsFromFile(f string) (string, error) { return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil } + +// GetMimeDetailsFromStream - Get media URL mime details. +func GetMimeDetailsFromStream(s io.ReadCloser) (string, error) { + defer s.Close() + head := make([]byte, 261) + s.Read(head) + + kind, err := filetype.Match(head) + if err != nil { + return "", fmt.Errorf("getMimeDetailsFromStream error: %w", err) + } + + return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil +} diff --git a/internal/utils/iptools.go b/internal/utils/iptools.go index 16bf8ac3..07fc4636 100644 --- a/internal/utils/iptools.go +++ b/internal/utils/iptools.go @@ -9,7 +9,8 @@ import ( "time" ) -// URLtoListenIPandPort for a given internal URL, find the correct IP/Interface to listen to. +// URLtoListenIPandPort for a given internal URL, +// find the correct IP/Interface to listen to. func URLtoListenIPandPort(u string) (string, error) { parsedURL, err := url.Parse(u) if err != nil { @@ -70,6 +71,6 @@ func HostPortIsAlive(h string) bool { if err != nil { return false } - defer conn.Close() + conn.Close() return true }