diff options
author | Simon Garrelou <simon.garrelou@gmail.com> | 2022-12-08 22:35:17 +0100 |
---|---|---|
committer | Simon Garrelou <simon.garrelou@gmail.com> | 2022-12-08 22:35:17 +0100 |
commit | 7e4333dac70cdb003e71b6805ed4d81f18aa233a (patch) | |
tree | 26190c147141f765d69880d98d891bf50071e891 | |
parent | 96cc5db2b4062ced82faf01ddac24abef04df343 (diff) | |
download | termsonic-7e4333dac70cdb003e71b6805ed4d81f18aa233a.tar.gz termsonic-7e4333dac70cdb003e71b6805ed4d81f18aa233a.zip |
Music playback working
-rw-r--r-- | cmd/main.go | 2 | ||||
-rw-r--r-- | go.mod | 30 | ||||
-rw-r--r-- | go.sum | 63 | ||||
-rw-r--r-- | music/patch.go | 76 | ||||
-rw-r--r-- | music/playqueue.go | 158 | ||||
-rw-r--r-- | src/app.go | 45 | ||||
-rw-r--r-- | src/footer.go | 9 | ||||
-rw-r--r-- | src/header.go | 47 | ||||
-rw-r--r-- | src/page_artists.go | 53 |
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 | ||
11 | var ( | 11 | var ( |
@@ -1,17 +1,35 @@ | |||
1 | module git.sixfoisneuf.fr/simon/termsonic | 1 | module git.sixfoisneuf.fr/termsonic |
2 | 2 | ||
3 | go 1.19 | 3 | go 1.19 |
4 | 4 | ||
5 | require ( | 5 | require ( |
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 | |||
14 | require ( | ||
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 | ) |
@@ -1,23 +1,81 @@ | |||
1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= | 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= |
2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= |
3 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= | ||
4 | github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= | ||
3 | github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= | 5 | github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= |
4 | github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= | 6 | github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= |
7 | github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= | ||
8 | github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= | ||
5 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= | 9 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= |
6 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | 10 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= |
11 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= | ||
7 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= | 12 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= |
8 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= | 13 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= |
14 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= | ||
15 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= | ||
16 | github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= | ||
17 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= | ||
18 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||
19 | github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g= | ||
20 | github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= | ||
21 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= | ||
22 | github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= | ||
23 | github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= | ||
24 | github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8= | ||
25 | github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= | ||
26 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= | ||
27 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= | ||
28 | github.com/jfbus/httprs v0.0.0-20190827093123-b0af8319bb15 h1:HPqgCwRiChGXITjjipDuTJYVPkAUpM4lp0mfo7ONpjo= | ||
29 | github.com/jfbus/httprs v0.0.0-20190827093123-b0af8319bb15/go.mod h1:hve3GCzwH1IcxgpZ3UN4XKAPSKoIqJhsYF2ZifruodQ= | ||
30 | github.com/jfreymuth/oggvorbis v1.0.1 h1:NT0eXBgE2WHzu6RT/6zcb2H10Kxj6Fm3PccT0LE6bqw= | ||
31 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= | ||
32 | github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U= | ||
33 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= | ||
34 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||
35 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||
36 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= | ||
9 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | 37 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= |
10 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | 38 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= |
39 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | ||
11 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= | 40 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= |
12 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | 41 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |
42 | github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8= | ||
43 | github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= | ||
44 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= | ||
45 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= | ||
46 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= | ||
47 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= | ||
48 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= | ||
49 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= | ||
50 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
51 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
52 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
13 | github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9 h1:ccTgRxA37ypj3q8zB8G4k3xGPfBbIaMwrf3Yw6k50NY= | 53 | github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9 h1:ccTgRxA37ypj3q8zB8G4k3xGPfBbIaMwrf3Yw6k50NY= |
14 | github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= | 54 | github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= |
15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= |
16 | github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= | 56 | github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= |
17 | github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | 57 | github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= |
58 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= | ||
59 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||
60 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= | ||
61 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | ||
62 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
63 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= | ||
64 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||
65 | golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= | ||
66 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= | ||
67 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= | ||
68 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= | ||
69 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= | ||
70 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
71 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||
72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
73 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
74 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
75 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
18 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
19 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= | 77 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew= |
20 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 78 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
21 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | 79 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
22 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= | 80 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= |
23 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | 81 | golang.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= | |||
26 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= | 84 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= |
27 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | 85 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= |
28 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | 86 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
87 | golang.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 @@ | |||
1 | package music | ||
2 | |||
3 | import ( | ||
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" | ||
16 | func 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 | |||
49 | func 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 @@ | |||
1 | package music | ||
2 | |||
3 | import ( | ||
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 | |||
16 | type 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 | |||
26 | func NewQueue(client *subsonic.Client) *Queue { | ||
27 | return &Queue{ | ||
28 | sub: client, | ||
29 | speakerInitialized: false, | ||
30 | } | ||
31 | } | ||
32 | |||
33 | func (q *Queue) SetClient(client *subsonic.Client) { | ||
34 | q.Clear() | ||
35 | q.sub = client | ||
36 | } | ||
37 | |||
38 | func (p *Queue) GetSongs() []*subsonic.Child { | ||
39 | return p.songs | ||
40 | } | ||
41 | |||
42 | func (q *Queue) Append(s *subsonic.Child) { | ||
43 | q.songs = append(q.songs, s) | ||
44 | } | ||
45 | |||
46 | func (q *Queue) Clear() { | ||
47 | q.songs = make([]*subsonic.Child, 0) | ||
48 | speaker.Clear() | ||
49 | } | ||
50 | |||
51 | func (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 | |||
91 | func (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 | |||
102 | func (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 | |||
121 | func (q *Queue) Stop() { | ||
122 | speaker.Clear() | ||
123 | } | ||
124 | |||
125 | func (q *Queue) SetOnChangeCallback(f func(newSong *subsonic.Child, isPlaying bool)) { | ||
126 | q.onChange = f | ||
127 | } | ||
128 | |||
129 | func (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 | |||
145 | func (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 | } | ||
@@ -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 | ||
12 | type app struct { | 13 | type 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 | ||
28 | func Run(cfg *Config) { | 31 | func 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 @@ | |||
1 | package src | 1 | package src |
2 | 2 | ||
3 | func (a *app) updateFooter() { | 3 | func (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 @@ | |||
1 | package src | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | |||
6 | "github.com/delucks/go-subsonic" | ||
7 | "github.com/rivo/tview" | ||
8 | ) | ||
9 | |||
10 | func (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 | ||
16 | func (a *app) artistsPage() tview.Primitive { | 17 | func (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 | } |