aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Garrelou <simon.garrelou@gmail.com>2022-12-08 22:35:17 +0100
committerSimon Garrelou <simon.garrelou@gmail.com>2022-12-08 22:35:17 +0100
commit7e4333dac70cdb003e71b6805ed4d81f18aa233a (patch)
tree26190c147141f765d69880d98d891bf50071e891
parent96cc5db2b4062ced82faf01ddac24abef04df343 (diff)
downloadtermsonic-7e4333dac70cdb003e71b6805ed4d81f18aa233a.tar.gz
termsonic-7e4333dac70cdb003e71b6805ed4d81f18aa233a.zip
Music playback working
-rw-r--r--cmd/main.go2
-rw-r--r--go.mod30
-rw-r--r--go.sum63
-rw-r--r--music/patch.go76
-rw-r--r--music/playqueue.go158
-rw-r--r--src/app.go45
-rw-r--r--src/footer.go9
-rw-r--r--src/header.go47
-rw-r--r--src/page_artists.go53
9 files changed, 430 insertions, 53 deletions
diff --git a/cmd/main.go b/cmd/main.go
index 53874e5..c1ff827 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -5,7 +5,7 @@ import (
5 "fmt" 5 "fmt"
6 "os" 6 "os"
7 7
8 "git.sixfoisneuf.fr/simon/termsonic/src" 8 "git.sixfoisneuf.fr/termsonic/src"
9) 9)
10 10
11var ( 11var (
diff --git a/go.mod b/go.mod
index 3a9d2e8..f4f3c78 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,35 @@
1module git.sixfoisneuf.fr/simon/termsonic 1module git.sixfoisneuf.fr/termsonic
2 2
3go 1.19 3go 1.19
4 4
5require ( 5require (
6 github.com/BurntSushi/toml v1.2.1 // indirect 6 github.com/BurntSushi/toml v1.2.1
7 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 // indirect 7 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5
8 github.com/faiface/beep v1.1.0
9 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
10 github.com/jfbus/httprs v0.0.0-20190827093123-b0af8319bb15
11 github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9
12)
13
14require (
8 github.com/gdamore/encoding v1.0.0 // indirect 15 github.com/gdamore/encoding v1.0.0 // indirect
9 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect 16 github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
17 github.com/hajimehoshi/oto v0.7.1 // indirect
18 github.com/icza/bitio v1.0.0 // indirect
19 github.com/jfreymuth/oggvorbis v1.0.1 // indirect
20 github.com/jfreymuth/vorbis v1.0.0 // indirect
10 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 21 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
11 github.com/mattn/go-runewidth v0.0.13 // indirect 22 github.com/mattn/go-runewidth v0.0.13 // indirect
12 github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9 // indirect 23 github.com/mewkiz/flac v1.0.7 // indirect
24 github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
25 github.com/mitchellh/copystructure v1.0.0 // indirect
26 github.com/mitchellh/reflectwalk v1.0.0 // indirect
27 github.com/pkg/errors v0.9.1 // indirect
13 github.com/rivo/uniseg v0.4.2 // indirect 28 github.com/rivo/uniseg v0.4.2 // indirect
14 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect 29 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
30 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
31 golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
32 golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
15 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 33 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
16 golang.org/x/text v0.3.7 // indirect 34 golang.org/x/text v0.3.7 // indirect
17) 35)
diff --git a/go.sum b/go.sum
index 43494fb..95da911 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,81 @@
1github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 1github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
2github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
4github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
3github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= 5github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg=
4github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= 6github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo=
7github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
8github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
5github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 9github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
6github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 10github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
11github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
7github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= 12github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
8github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= 13github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
14github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
15github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
16github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
17github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
18github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
19github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g=
20github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
21github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
22github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
23github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
24github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
25github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
26github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
27github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
28github.com/jfbus/httprs v0.0.0-20190827093123-b0af8319bb15 h1:HPqgCwRiChGXITjjipDuTJYVPkAUpM4lp0mfo7ONpjo=
29github.com/jfbus/httprs v0.0.0-20190827093123-b0af8319bb15/go.mod h1:hve3GCzwH1IcxgpZ3UN4XKAPSKoIqJhsYF2ZifruodQ=
30github.com/jfreymuth/oggvorbis v1.0.1 h1:NT0eXBgE2WHzu6RT/6zcb2H10Kxj6Fm3PccT0LE6bqw=
31github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
32github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
33github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
34github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
35github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
36github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
9github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 37github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
10github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 38github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
39github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
11github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 40github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
12github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 41github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
42github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
43github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
44github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU=
45github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
46github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
47github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
48github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
49github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
50github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
51github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
52github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
13github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9 h1:ccTgRxA37ypj3q8zB8G4k3xGPfBbIaMwrf3Yw6k50NY= 53github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9 h1:ccTgRxA37ypj3q8zB8G4k3xGPfBbIaMwrf3Yw6k50NY=
14github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= 54github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
15github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 55github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
16github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= 56github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
17github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 57github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
58github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
59github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
60github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
61github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
62golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
63golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
64golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
65golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
66golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
67golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
68golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
69golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
70golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
71golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
72golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
73golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
74golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
75golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
18golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= 77golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
20golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
21golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 79golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
22golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= 80golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
23golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 81golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -26,3 +84,4 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
26golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 84golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
27golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 85golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
28golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
87golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
diff --git a/music/patch.go b/music/patch.go
new file mode 100644
index 0000000..aba20ec
--- /dev/null
+++ b/music/patch.go
@@ -0,0 +1,76 @@
1package music
2
3import (
4 "encoding/xml"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "net/url"
9 "strings"
10
11 "github.com/delucks/go-subsonic"
12 "github.com/jfbus/httprs"
13)
14
15// Stream2 patches subsonic.Client.Stream to return a ReadCloser for use with "beep"
16func Stream2(s *subsonic.Client, id string, parameters map[string]string) (io.ReadCloser, error) {
17 params := url.Values{}
18 params.Add("id", id)
19 for k, v := range parameters {
20 params.Add(k, v)
21 }
22 response, err := s.Request("GET", "stream", params)
23 if err != nil {
24 return nil, err
25 }
26 contentType := response.Header.Get("Content-Type")
27 if strings.HasPrefix(contentType, "text/xml") || strings.HasPrefix(contentType, "application/xml") {
28 // An error was returned
29 responseBody, err := ioutil.ReadAll(response.Body)
30 if err != nil {
31 return nil, err
32 }
33 resp := subsonic.Response{}
34 err = xml.Unmarshal(responseBody, &resp)
35 if err != nil {
36 return nil, err
37 }
38 if resp.Error != nil {
39 err = fmt.Errorf("Error #%d: %s\n", resp.Error.Code, resp.Error.Message)
40 } else {
41 err = fmt.Errorf("An error occurred: %#v\n", resp)
42 }
43 return nil, err
44 }
45
46 return httprs.NewHttpReadSeeker(response), nil
47}
48
49func Download2(s *subsonic.Client, id string) (io.ReadCloser, error) {
50 params := url.Values{}
51 params.Add("id", id)
52 response, err := s.Request("GET", "download", params)
53 if err != nil {
54 return nil, err
55 }
56 contentType := response.Header.Get("Content-Type")
57 if strings.HasPrefix(contentType, "text/xml") || strings.HasPrefix(contentType, "application/xml") {
58 // An error was returned
59 responseBody, err := ioutil.ReadAll(response.Body)
60 if err != nil {
61 return nil, err
62 }
63 resp := subsonic.Response{}
64 err = xml.Unmarshal(responseBody, &resp)
65 if err != nil {
66 return nil, err
67 }
68 if resp.Error != nil {
69 err = fmt.Errorf("Error #%d: %s\n", resp.Error.Code, resp.Error.Message)
70 } else {
71 err = fmt.Errorf("An error occurred: %#v\n", resp)
72 }
73 return nil, err
74 }
75 return httprs.NewHttpReadSeeker(response), nil
76}
diff --git a/music/playqueue.go b/music/playqueue.go
new file mode 100644
index 0000000..d6182ed
--- /dev/null
+++ b/music/playqueue.go
@@ -0,0 +1,158 @@
1package music
2
3import (
4 "fmt"
5 "path/filepath"
6 "time"
7
8 "github.com/delucks/go-subsonic"
9 "github.com/faiface/beep"
10 "github.com/faiface/beep/flac"
11 "github.com/faiface/beep/mp3"
12 "github.com/faiface/beep/speaker"
13 "github.com/faiface/beep/vorbis"
14)
15
16type Queue struct {
17 songs []*subsonic.Child
18 isPaused bool
19
20 sub *subsonic.Client
21 speakerInitialized bool
22 oldSampleRate beep.SampleRate
23 onChange func(newSong *subsonic.Child, isPaused bool)
24}
25
26func NewQueue(client *subsonic.Client) *Queue {
27 return &Queue{
28 sub: client,
29 speakerInitialized: false,
30 }
31}
32
33func (q *Queue) SetClient(client *subsonic.Client) {
34 q.Clear()
35 q.sub = client
36}
37
38func (p *Queue) GetSongs() []*subsonic.Child {
39 return p.songs
40}
41
42func (q *Queue) Append(s *subsonic.Child) {
43 q.songs = append(q.songs, s)
44}
45
46func (q *Queue) Clear() {
47 q.songs = make([]*subsonic.Child, 0)
48 speaker.Clear()
49}
50
51func (q *Queue) PlaySong(s *subsonic.Child) error {
52 rc, err := Download2(q.sub, s.ID)
53 if err != nil {
54 return err
55 }
56
57 var ssc beep.StreamSeekCloser
58 var format beep.Format
59
60 switch filepath.Ext(s.Path) {
61 case ".mp3":
62 ssc, format, err = mp3.Decode(rc)
63 case ".ogg":
64 fallthrough
65 case ".oga":
66 ssc, format, err = vorbis.Decode(rc)
67 case ".flac":
68 ssc, format, err = flac.Decode(rc)
69 default:
70 return fmt.Errorf("unknown file type: %s", filepath.Ext(s.Path))
71 }
72
73 if err != nil {
74 return err
75 }
76
77 streamer, err := q.setupSpeaker(ssc, format)
78 if err != nil {
79 return err
80 }
81 speaker.Clear()
82 speaker.Play(beep.Seq(streamer, beep.Callback(func() { go q.Next() })))
83
84 if q.onChange != nil {
85 q.onChange(s, false)
86 }
87
88 return nil
89}
90
91func (q *Queue) Play() error {
92 if len(q.songs) == 0 {
93 return fmt.Errorf("the queue is empty")
94 }
95
96 s := q.songs[0]
97 q.PlaySong(s)
98
99 return nil
100}
101
102func (q *Queue) Next() error {
103 q.Stop()
104
105 if len(q.songs) == 0 {
106 return nil
107 }
108
109 q.songs = q.songs[1:]
110
111 if len(q.songs) == 0 {
112 if q.onChange != nil {
113 q.onChange(nil, false)
114 }
115 return nil
116 }
117
118 return q.Play()
119}
120
121func (q *Queue) Stop() {
122 speaker.Clear()
123}
124
125func (q *Queue) SetOnChangeCallback(f func(newSong *subsonic.Child, isPlaying bool)) {
126 q.onChange = f
127}
128
129func (q *Queue) TogglePause() {
130 if q.isPaused {
131 speaker.Unlock()
132 } else {
133 speaker.Lock()
134 }
135
136 q.isPaused = !q.isPaused
137
138 if q.onChange != nil {
139 if len(q.songs) > 0 {
140 q.onChange(q.songs[0], q.isPaused)
141 }
142 }
143}
144
145func (p *Queue) setupSpeaker(s beep.Streamer, format beep.Format) (beep.Streamer, error) {
146 if !p.speakerInitialized {
147 err := speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
148 if err != nil {
149 return nil, fmt.Errorf("speaker.Init: %v", err)
150 }
151 p.speakerInitialized = true
152 p.oldSampleRate = format.SampleRate
153
154 return s, nil
155 } else {
156 return beep.Resample(4, format.SampleRate, p.oldSampleRate, s), nil
157 }
158}
diff --git a/src/app.go b/src/app.go
index 8eed517..029644c 100644
--- a/src/app.go
+++ b/src/app.go
@@ -4,6 +4,7 @@ import (
4 "fmt" 4 "fmt"
5 "os" 5 "os"
6 6
7 "git.sixfoisneuf.fr/termsonic/music"
7 "github.com/delucks/go-subsonic" 8 "github.com/delucks/go-subsonic"
8 "github.com/gdamore/tcell/v2" 9 "github.com/gdamore/tcell/v2"
9 "github.com/rivo/tview" 10 "github.com/rivo/tview"
@@ -11,23 +12,26 @@ import (
11 12
12type app struct { 13type app struct {
13 // General GUI 14 // General GUI
14 tv *tview.Application 15 tv *tview.Application
15 pages *tview.Pages 16 pages *tview.Pages
16 header *tview.TextView 17 headerSections *tview.TextView
17 footer *tview.TextView 18 headerNowPlaying *tview.TextView
18 cfg *Config 19 footer *tview.TextView
20 cfg *Config
19 21
20 // Artists panel 22 // Artists panel
21 artistsTree *tview.TreeView 23 artistsTree *tview.TreeView
22 songsList *tview.List 24 songsList *tview.List
23 25
24 // Subsonic variables 26 // Subsonic variables
25 sub *subsonic.Client 27 sub *subsonic.Client
28 playQueue *music.Queue
26} 29}
27 30
28func Run(cfg *Config) { 31func Run(cfg *Config) {
29 a := &app{ 32 a := &app{
30 cfg: cfg, 33 cfg: cfg,
34 playQueue: music.NewQueue(nil),
31 } 35 }
32 36
33 a.tv = tview.NewApplication() 37 a.tv = tview.NewApplication()
@@ -35,28 +39,13 @@ func Run(cfg *Config) {
35 a.footer = tview.NewTextView(). 39 a.footer = tview.NewTextView().
36 SetDynamicColors(true) 40 SetDynamicColors(true)
37 41
38 a.header = tview.NewTextView().
39 SetRegions(true).
40 SetChangedFunc(func() {
41 a.tv.Draw()
42 }).
43 SetHighlightedFunc(func(added, _, _ []string) {
44 hl := added[0]
45 cur, _ := a.pages.GetFrontPage()
46
47 if hl != cur {
48 a.switchToPage(hl)
49 }
50 })
51 fmt.Fprintf(a.header, `["artists"]F1: Artists[""] | ["playlists"]F2: Playlists[""] | ["config"]F3: Configuration[""]`)
52
53 a.pages.SetBorder(true) 42 a.pages.SetBorder(true)
54 a.pages.AddPage("config", a.configPage(), true, false) 43 a.pages.AddPage("config", a.configPage(), true, false)
55 a.pages.AddPage("artists", a.artistsPage(), true, false) 44 a.pages.AddPage("artists", a.artistsPage(), true, false)
56 45
57 mainLayout := tview.NewFlex(). 46 mainLayout := tview.NewFlex().
58 SetDirection(tview.FlexRow). 47 SetDirection(tview.FlexRow).
59 AddItem(a.header, 1, 1, false). 48 AddItem(a.buildHeader(), 1, 1, false).
60 AddItem(a.pages, 0, 3, true). 49 AddItem(a.pages, 0, 3, true).
61 AddItem(a.footer, 1, 1, false) 50 AddItem(a.footer, 1, 1, false)
62 51
@@ -64,6 +53,7 @@ func Run(cfg *Config) {
64 a.switchToPage("config") 53 a.switchToPage("config")
65 } else { 54 } else {
66 a.sub, _ = buildSubsonicClient(a.cfg) 55 a.sub, _ = buildSubsonicClient(a.cfg)
56 a.playQueue.SetClient(a.sub)
67 err := a.refreshArtists() 57 err := a.refreshArtists()
68 if err != nil { 58 if err != nil {
69 a.alert("Could not refresh artists: %v", err) 59 a.alert("Could not refresh artists: %v", err)
@@ -104,14 +94,17 @@ func (a *app) switchToPage(name string) {
104 switch name { 94 switch name {
105 case "artists": 95 case "artists":
106 a.pages.SwitchToPage("artists") 96 a.pages.SwitchToPage("artists")
107 a.header.Highlight("artists") 97 a.headerSections.Highlight("artists")
108 a.tv.SetFocus(a.artistsTree) 98 a.tv.SetFocus(a.artistsTree)
99 a.pages.SetBorder(false)
109 case "playlists": 100 case "playlists":
110 a.pages.SwitchToPage("playlists") 101 a.pages.SwitchToPage("playlists")
111 a.header.Highlight("playlists") 102 a.headerSections.Highlight("playlists")
103 a.pages.SetBorder(true)
112 case "config": 104 case "config":
113 a.pages.SwitchToPage("config") 105 a.pages.SwitchToPage("config")
114 a.header.Highlight("config") 106 a.headerSections.Highlight("config")
107 a.pages.SetBorder(true)
115 } 108 }
116 109
117 a.updateFooter() 110 a.updateFooter()
diff --git a/src/footer.go b/src/footer.go
index b0494d0..2695a61 100644
--- a/src/footer.go
+++ b/src/footer.go
@@ -1,14 +1,9 @@
1package src 1package src
2 2
3func (a *app) updateFooter() { 3func (a *app) updateFooter() {
4 switch a.header.GetHighlights()[0] { 4 switch a.headerSections.GetHighlights()[0] {
5 case "artists": 5 case "artists":
6 switch a.tv.GetFocus() { 6 a.footer.SetText("[blue]l:[yellow] Next song [blue]k:[yellow] Toggle pause")
7 case a.artistsTree:
8 a.footer.SetText("Artists: [blue]Up/Down:[yellow] Move selection [blue]Space:[yellow] Select entry")
9 case a.songsList:
10 a.footer.SetText("Songs: [blue]Up/Down:[yellow] Move selection [blue]Space:[yellow] Play")
11 }
12 case "playlists": 7 case "playlists":
13 a.footer.SetText("Come back later!") 8 a.footer.SetText("Come back later!")
14 case "config": 9 case "config":
diff --git a/src/header.go b/src/header.go
new file mode 100644
index 0000000..5c85808
--- /dev/null
+++ b/src/header.go
@@ -0,0 +1,47 @@
1package src
2
3import (
4 "fmt"
5
6 "github.com/delucks/go-subsonic"
7 "github.com/rivo/tview"
8)
9
10func (a *app) buildHeader() tview.Primitive {
11 flex := tview.NewFlex()
12 flex.SetDirection(tview.FlexColumn)
13
14 a.headerSections = tview.NewTextView().
15 SetRegions(true).
16 SetChangedFunc(func() {
17 a.tv.Draw()
18 }).
19 SetHighlightedFunc(func(added, _, _ []string) {
20 hl := added[0]
21 cur, _ := a.pages.GetFrontPage()
22
23 if hl != cur {
24 a.switchToPage(hl)
25 }
26 })
27 fmt.Fprintf(a.headerSections, `["artists"]F1: Artists[""] | ["playlists"]F2: Playlists[""] | ["config"]F3: Configuration[""]`)
28
29 a.headerNowPlaying = tview.NewTextView().SetTextAlign(tview.AlignRight)
30
31 flex.AddItem(a.headerSections, 0, 1, false)
32 flex.AddItem(a.headerNowPlaying, 0, 1, false)
33
34 a.playQueue.SetOnChangeCallback(func(song *subsonic.Child, isPaused bool) {
35 if song != nil {
36 symbol := ">"
37 if isPaused {
38 symbol = "||"
39 }
40 a.headerNowPlaying.SetText(fmt.Sprintf("%s %s - %s", symbol, song.Title, song.Artist))
41 } else {
42 a.headerNowPlaying.SetText("Not playing")
43 }
44 })
45
46 return flex
47}
diff --git a/src/page_artists.go b/src/page_artists.go
index e8ce180..4361767 100644
--- a/src/page_artists.go
+++ b/src/page_artists.go
@@ -4,6 +4,7 @@ import (
4 "fmt" 4 "fmt"
5 "time" 5 "time"
6 6
7 "github.com/delucks/go-subsonic"
7 "github.com/gdamore/tcell/v2" 8 "github.com/gdamore/tcell/v2"
8 "github.com/rivo/tview" 9 "github.com/rivo/tview"
9) 10)
@@ -14,9 +15,7 @@ type selection struct {
14} 15}
15 16
16func (a *app) artistsPage() tview.Primitive { 17func (a *app) artistsPage() tview.Primitive {
17 grid := tview.NewGrid(). 18 grid := tview.NewFlex().SetDirection(tview.FlexColumn)
18 SetColumns(40, 0).
19 SetBorders(true)
20 19
21 // Artist & album list 20 // Artist & album list
22 root := tview.NewTreeNode("Subsonic server").SetColor(tcell.ColorYellow) 21 root := tview.NewTreeNode("Subsonic server").SetColor(tcell.ColorYellow)
@@ -37,18 +36,18 @@ func (a *app) artistsPage() tview.Primitive {
37 36
38 a.loadAlbumInPanel(sel.id) 37 a.loadAlbumInPanel(sel.id)
39 a.tv.SetFocus(a.songsList) 38 a.tv.SetFocus(a.songsList)
40 a.updateFooter()
41 }) 39 })
40 a.artistsTree.SetBorderAttributes(tcell.AttrDim).SetBorder(true)
42 41
43 // Songs list for the selected album 42 // Songs list for the selected album
44 a.songsList = tview.NewList() 43 a.songsList = tview.NewList()
45 a.songsList.ShowSecondaryText(false) 44 a.songsList.ShowSecondaryText(false)
45 a.songsList.SetBorderAttributes(tcell.AttrDim).SetBorder(true)
46 46
47 // Change the left-right keys to switch between the panels 47 // Change the left-right keys to switch between the panels
48 a.artistsTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 48 a.artistsTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
49 if event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight { 49 if event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight {
50 a.tv.SetFocus(a.songsList) 50 a.tv.SetFocus(a.songsList)
51 a.updateFooter()
52 return nil 51 return nil
53 } 52 }
54 return event 53 return event
@@ -57,14 +56,26 @@ func (a *app) artistsPage() tview.Primitive {
57 a.songsList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 56 a.songsList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
58 if event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight { 57 if event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight {
59 a.tv.SetFocus(a.artistsTree) 58 a.tv.SetFocus(a.artistsTree)
60 a.updateFooter()
61 return nil 59 return nil
62 } 60 }
63 return event 61 return event
64 }) 62 })
65 63
66 grid.AddItem(a.artistsTree, 0, 0, 1, 1, 0, 0, true) 64 grid.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
67 grid.AddItem(a.songsList, 0, 1, 1, 2, 0, 0, false) 65 if event.Rune() == 'l' {
66 a.playQueue.Next()
67 return nil
68 }
69
70 if event.Rune() == 'k' {
71 a.playQueue.TogglePause()
72 return nil
73 }
74 return event
75 })
76
77 grid.AddItem(a.artistsTree, 0, 1, true)
78 grid.AddItem(a.songsList, 0, 1, false)
68 79
69 return grid 80 return grid
70} 81}
@@ -113,12 +124,32 @@ func (a *app) loadAlbumInPanel(id string) error {
113 return err 124 return err
114 } 125 }
115 126
116 a.songsList.SetTitle(album.Name) 127 var songs []*subsonic.Child
128
117 a.songsList.Clear() 129 a.songsList.Clear()
118 for _, song := range album.Child { 130 for i := len(album.Child) - 1; i >= 0; i-- {
131 song := album.Child[i]
132 songNoPtr := *song
133 songs = append([]*subsonic.Child{&songNoPtr}, songs...)
134
135 songsCopy := make([]*subsonic.Child, len(songs))
136 copy(songsCopy, songs)
137
119 dur := time.Duration(song.Duration) * time.Second 138 dur := time.Duration(song.Duration) * time.Second
120 a.songsList.AddItem(fmt.Sprintf("%-10s %d - %s", fmt.Sprintf("[%s]", dur.String()), song.Track, song.Title), "", 0, nil) 139
140 a.songsList.InsertItem(0, fmt.Sprintf("%-10s %d - %s", fmt.Sprintf("[%s]", dur.String()), song.Track, song.Title), "", 0, func() {
141 a.playQueue.Clear()
142 for _, s := range songsCopy {
143 a.playQueue.Append(s)
144 }
145 err := a.playQueue.Play()
146 if err != nil {
147 a.alert("Error: %v", err)
148 }
149 })
121 } 150 }
122 151
152 a.songsList.SetCurrentItem(0)
153
123 return nil 154 return nil
124} 155}