209 Commits

Author SHA1 Message Date
19073d7fba Update module golang.org/x/sync to v0.17.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-09-07 13:02:23 +00:00
2fcc42597d Merge pull request 'Update module tailscale.com to v1.84.3' (#74) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #74
2025-06-29 18:52:44 +00:00
f3f8995d53 Update module tailscale.com to v1.84.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-26 17:02:02 +00:00
6c9320efae Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.9.3' (#73) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #73
2025-06-13 20:14:18 +00:00
c9131bded8 Update module github.com/go-sql-driver/mysql to v1.9.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-13 07:01:34 +00:00
e1fe23bcba Merge pull request 'Update module tailscale.com to v1.84.2' (#72) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #72
2025-06-10 14:07:48 +00:00
d3febf1f6c Update module tailscale.com to v1.84.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-09 22:02:14 +00:00
0fe9679891 Merge pull request 'Update module golang.org/x/sync to v0.15.0' (#71) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #71
2025-06-09 01:24:07 +00:00
03c835139f Update module golang.org/x/sync to v0.15.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-06 19:01:59 +00:00
af42df691b Merge pull request 'remove tui, unused' (#70) from remove-tui into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #70
2025-06-06 18:46:59 +00:00
95b4fc1802 remove tui, unused
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-06-04 22:04:51 -04:00
3874b2cc9d Merge pull request 'Update module tailscale.com to v1.84.1' (#69) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #69
2025-06-05 01:58:13 +00:00
049e55c9c9 Update module tailscale.com to v1.84.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-05-29 18:02:23 +00:00
a370265e2e Merge pull request 'Update mysql Docker tag to v9.3' (#44) from renovate/mysql-9.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #44
2025-05-26 04:13:26 +00:00
535dd86aba Merge pull request 'Update module golang.org/x/sync to v0.14.0' (#67) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #67
2025-05-23 02:16:02 +00:00
b0896539c8 Update module golang.org/x/sync to v0.14.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-05-23 01:06:13 +00:00
28d9c9b659 Merge pull request 'Update module tailscale.com to v1.84.0' (#68) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #68
2025-05-23 00:55:32 +00:00
6f9e1ec589 Update module tailscale.com to v1.84.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-05-21 20:02:45 +00:00
2adbe3004f Merge pull request 'Update module tailscale.com to v1.82.5' (#66) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #66
2025-05-14 02:10:47 +00:00
da9fbaf347 Update module tailscale.com to v1.82.5
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-17 20:02:47 +00:00
5bca510e83 Update mysql Docker tag to v9.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-15 04:01:58 +00:00
e76c63ccdc Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.9.2' (#65) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #65
2025-04-07 15:21:09 +00:00
288c345a22 Update module github.com/go-sql-driver/mysql to v1.9.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-07 12:02:07 +00:00
151265cc0e Merge pull request 'Update module golang.org/x/sync to v0.13.0' (#64) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #64
2025-04-06 16:59:49 +00:00
91cd6a1810 Update module golang.org/x/sync to v0.13.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-04-05 14:02:09 +00:00
64b767d0b5 Merge pull request 'Update module tailscale.com to v1.82.0' (#63) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #63
2025-03-30 20:11:08 +00:00
94de300e16 1.24 upgrade
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-03-30 15:36:04 -04:00
813f8a43b2 Update module tailscale.com to v1.82.0
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-03-26 21:03:02 +00:00
d219f2b142 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.9.1' (#62) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #62
2025-03-25 01:44:01 +00:00
bbc4a5fecf Update module github.com/go-sql-driver/mysql to v1.9.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-21 03:02:13 +00:00
20de1eccbb Merge pull request 'Update module golang.org/x/sync to v0.12.0' (#61) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #61
2025-03-19 20:16:20 +00:00
7076c40e63 Update module golang.org/x/sync to v0.12.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-05 14:03:29 +00:00
7fafd997ca Merge pull request 'Update module tailscale.com to v1.80.3' (#60) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #60
2025-03-04 15:57:19 +00:00
007c4a5954 Update module tailscale.com to v1.80.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-04 00:02:45 +00:00
c39837fa0a Merge pull request 'Update module tailscale.com to v1.80.2' (#57) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #57
2025-02-19 03:02:48 +00:00
956051a8b5 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.9.0' (#59) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #59
2025-02-18 14:46:15 +00:00
81bdb3d4d8 Update module github.com/go-sql-driver/mysql to v1.9.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-02-18 04:03:02 +00:00
33354dc21a Update module tailscale.com to v1.80.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-02-12 19:03:58 +00:00
37682d66c4 Merge pull request 'Update module tailscale.com to v1.80.1' (#56) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #56
2025-02-09 03:17:38 +00:00
bd6831233e Update module tailscale.com to v1.80.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-02-06 19:03:20 +00:00
dcf5508576 Merge pull request 'Update module golang.org/x/sync to v0.11.0' (#55) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #55
2025-02-06 14:28:14 +00:00
c996dcaef2 Update module golang.org/x/sync to v0.11.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-02-04 15:02:25 +00:00
0e09e47694 Merge pull request 'Update module github.com/gdamore/tcell/v2 to v2.8.1' (#53) from renovate/github.com-gdamore-tcell-v2-2.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #53
2025-02-04 03:46:05 +00:00
3d4d10f11c implement missing mock methods
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-02-03 22:41:59 -05:00
ae0eb5f001 Update module github.com/gdamore/tcell/v2 to v2.8.1
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-02-04 03:03:05 +00:00
d36a89f61c Merge pull request 'Update module tailscale.com to v1.80.0' (#54) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #54
2025-02-04 02:23:21 +00:00
441809573e Update module tailscale.com to v1.80.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-30 22:02:59 +00:00
671bffdb2f update secrets syntax
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-05 19:08:51 +00:00
a3413934b8 Merge pull request 'Update module golang.org/x/sync to v0.10.0' (#50) from renovate/golang.org-x-sync-0.x into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline failed
Reviewed-on: #50
2024-12-18 14:03:03 +00:00
5fb1786b5a Update module golang.org/x/sync to v0.10.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-15 04:08:01 +00:00
8293b7b384 Merge pull request 'Update module tailscale.com to v1.78.3' (#52) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #52
2024-12-15 03:45:04 +00:00
db543392d9 Update module tailscale.com to v1.78.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-11 21:03:17 +00:00
405063d20b Merge pull request 'Update module tailscale.com to v1.78.1' (#51) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #51
2024-12-07 21:34:22 +00:00
030a403641 Update module tailscale.com to v1.78.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-06 01:03:08 +00:00
935956c7bf Merge pull request 'Update module tailscale.com to v1.76.6' (#47) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #47
2024-11-10 19:21:46 +00:00
d9d57c4b2b Update module tailscale.com to v1.76.6
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-11-09 21:02:21 +00:00
155f5a967f Merge pull request 'Update module golang.org/x/sync to v0.9.0' (#49) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #49
2024-11-09 20:30:51 +00:00
de3a121b3d Update module golang.org/x/sync to v0.9.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-11-07 22:02:19 +00:00
7175484edb Merge pull request 'Update module tailscale.com to v1.76.3' (#46) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #46
2024-10-29 15:27:49 +00:00
9a1974a538 Update module tailscale.com to v1.76.3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-22 01:02:47 +00:00
757112837d Merge pull request 'Update module tailscale.com to v1.76.1' (#45) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #45
2024-10-22 00:37:13 +00:00
990d18bc64 Update module tailscale.com to v1.76.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-17 19:02:48 +00:00
cd9c4e72ac Merge pull request 'Update module tailscale.com to v1.76.0' (#43) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #43
2024-10-13 18:23:56 +00:00
2ee8278652 Update module tailscale.com to v1.76.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-10 19:02:34 +00:00
d4f021ff89 use proper host
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-09-23 14:06:38 +00:00
89291c1c0a Merge pull request 'Update module tailscale.com to v1.74.1' (#41) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #41
2024-09-21 18:06:26 +00:00
bd7cf08fcb Update module tailscale.com to v1.74.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-09-18 20:02:23 +00:00
dc556e210e Merge pull request 'Update module tailscale.com to v1.74.0' (#40) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #40
2024-09-14 17:16:41 +00:00
734f2d094f Update module tailscale.com to v1.74.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-09-12 21:02:17 +00:00
aca9b1f672 Merge pull request 'Update module tailscale.com to v1.72.1' (#39) from renovate/tailscale.com-1.x into master
Reviewed-on: #39
2024-09-03 12:26:35 +00:00
b5f04fb3dc Update module tailscale.com to v1.72.1 2024-09-02 18:02:49 +00:00
eef2505285 Merge pull request 'Update golang Docker tag to v1.23' (#38) from renovate/golang-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #38
2024-08-20 00:49:33 +00:00
a6887df550 Update golang Docker tag to v1.23
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-08-13 21:01:35 +00:00
c5b4464a59 Merge pull request 'Update module golang.org/x/sync to v0.8.0' (#37) from renovate/golang.org-x-sync-0.x into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: #37
2024-08-08 12:21:46 +00:00
b641ac0ca6 Update module golang.org/x/sync to v0.8.0 2024-08-04 16:04:37 +00:00
29434549ce Merge pull request 'Update mysql Docker tag to v9' (#36) from renovate/mysql-9.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #36
2024-07-12 13:49:12 +00:00
c086a0ccb1 upgrade tsnet
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-08 08:46:16 -04:00
7bb73b8023 Update mysql Docker tag to v9
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-02 22:01:32 +00:00
cdeb5e2d84 Merge pull request 'Update mysql Docker tag to v8.4' (#20) from renovate/mysql-8.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #20
2024-06-09 14:47:50 +00:00
551f1ef203 Merge pull request 'Update module tailscale.com to v1.66.4' (#35) from renovate/tailscale.com-1.x into master
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is running
Reviewed-on: #35
2024-06-09 14:47:37 +00:00
5ef60f70f1 Update module tailscale.com to v1.66.4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-21 01:02:06 +00:00
f5d8ad7c8c Update mysql Docker tag to v8.4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-01 01:01:28 +00:00
b1708ec1a8 Merge pull request 'Update module golang.org/x/sync to v0.7.0' (#32) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #32
2024-04-22 00:47:04 +00:00
94a9a0b77e Update module golang.org/x/sync to v0.7.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-18 18:01:37 +00:00
0ab60f1297 Merge pull request 'Update module tailscale.com to v1.64.2' (#34) from renovate/tailscale.com-1.x into master
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is running
Reviewed-on: #34
2024-04-18 17:35:51 +00:00
b68ac7f643 Update module tailscale.com to v1.64.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-17 15:01:47 +00:00
5f4da44bf0 clean up go mod file 2024-04-13 13:45:35 -04:00
0a337e42dd Merge pull request 'Update module tailscale.com to v1.64.0' (#33) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #33
2024-04-13 13:59:38 +00:00
165a913828 Update module tailscale.com to v1.64.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-11 20:02:11 +00:00
aec3b2dff4 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.8.1' (#30) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #30
2024-03-29 19:00:45 +00:00
cf8b07725b Merge pull request 'Update module tailscale.com to v1.62.1' (#31) from renovate/tailscale.com-1.x into master
Reviewed-on: #31
2024-03-29 18:59:40 +00:00
467d8202e2 Update module tailscale.com to v1.62.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-26 21:01:40 +00:00
2b6024e229 Update module github.com/go-sql-driver/mysql to v1.8.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-26 15:01:31 +00:00
62d03849a0 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.8.0' (#28) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #28
2024-03-18 00:15:25 +00:00
7610034240 Merge pull request 'Update module tailscale.com to v1.62.0' (#29) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #29
2024-03-18 00:12:41 +00:00
7a6f5740e0 Update module tailscale.com to v1.62.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-13 16:01:38 +00:00
09478cbd5b Update module github.com/go-sql-driver/mysql to v1.8.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-09 08:01:26 +00:00
609d6cf166 Merge pull request 'Update module tailscale.com to v1.60.1' (#25) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #25
2024-03-04 01:43:16 +00:00
55b6c0689d Merge pull request 'Update module github.com/gdamore/tcell/v2 to v2.7.4' (#26) from renovate/github.com-gdamore-tcell-v2-2.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #26
2024-03-04 01:40:58 +00:00
363dc85eb4 Update module github.com/gdamore/tcell/v2 to v2.7.4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-04 00:01:29 +00:00
c9b9805cee Update module tailscale.com to v1.60.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-29 22:01:32 +00:00
bf3c3d1dd7 bump build go
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-25 01:42:16 +00:00
e3e8d68c95 upgrade go
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-25 01:39:41 +00:00
960b0b8766 use steps
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-25 01:39:13 +00:00
f7b2e32651 Merge pull request 'Update module github.com/gdamore/tcell/v2 to v2.7.1' (#24) from renovate/github.com-gdamore-tcell-v2-2.x into master
Reviewed-on: #24
2024-02-21 01:23:44 +00:00
1554b151cb Update module github.com/gdamore/tcell/v2 to v2.7.1 2024-02-20 16:01:27 +00:00
a191e41453 Merge pull request 'Update module tailscale.com to v1.60.0' (#23) from renovate/tailscale.com-1.x into master
Reviewed-on: #23
2024-02-20 15:22:19 +00:00
0848406a85 Update module tailscale.com to v1.60.0 2024-02-15 21:01:58 +00:00
0ce38e5453 Merge pull request 'Update module tailscale.com to v1.58.2' (#21) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #21
2024-01-27 18:04:29 +00:00
bd1bf93ea0 Update module tailscale.com to v1.58.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-23 23:01:31 +00:00
c26e9513ad Merge pull request 'Update module tailscale.com to v1.58.0' (#19) from renovate/tailscale.com-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #19
2024-01-20 01:55:46 +00:00
cb53a35de6 Update module tailscale.com to v1.58.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-19 02:02:36 +00:00
d69d416c49 Merge pull request 'Update module golang.org/x/sync to v0.6.0' (#18) from renovate/golang.org-x-sync-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #18
2024-01-07 04:13:01 +00:00
caba03e58a Update module golang.org/x/sync to v0.6.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-07 04:01:32 +00:00
315fb4e9d2 management server listener (#17)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #17
Co-authored-by: David Ashby <delta.mu.alpha@gmail.com>
Co-committed-by: David Ashby <delta.mu.alpha@gmail.com>
2024-01-06 21:38:13 +00:00
dc7218131d Merge pull request 'Update module github.com/gdamore/tcell/v2 to v2.7.0' (#16) from renovate/github.com-gdamore-tcell-v2-2.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #16
2023-12-24 20:06:45 +00:00
878635450c add methods to mockscreen
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-24 15:05:11 -05:00
138a4a62c1 Update module github.com/gdamore/tcell/v2 to v2.7.0
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-12-14 22:01:33 +00:00
6658edfd09 Merge pull request 'Update mysql Docker tag to v8.2' (#15) from renovate/mysql-8.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #15
2023-10-27 14:17:12 +00:00
4a13dc5e7f Update mysql Docker tag to v8.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-27 02:01:19 +00:00
43c3a25758 make root filter work with children's books
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 17:08:55 -04:00
c30052bac7 rollback and fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 17:01:22 -04:00
e8c3da4ac8 re-render when children's checkbox toggles
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 16:57:22 -04:00
f282b10c05 hide empty strings in isbns
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 16:42:46 -04:00
95b269ca05 display isbn10s as well
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 15:37:36 -04:00
3d2c9964dc style label
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-08 15:39:03 -04:00
9d3a6fc876 fix checkbox label
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-08 15:36:58 -04:00
77ddc7ec8e add 'childrens' as a field on books
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-08 15:05:15 -04:00
1069dadd10 add flag for if a book is a children's book
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-08 10:21:52 -04:00
27c3a5c881 Merge pull request 'Update golang Docker tag to v1.21' (#14) from renovate/golang-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #14
2023-08-10 17:52:58 +00:00
899ad531a4 Update golang Docker tag to v1.21
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-08 22:01:12 +00:00
d2b68c1889 Merge pull request 'Update mysql Docker tag to v8.1' (#13) from renovate/mysql-8.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #13
2023-08-05 01:18:23 +00:00
727dd7ae6f Update mysql Docker tag to v8.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-03 01:01:07 +00:00
c2d9bde962 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.7.1' (#12) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #12
2023-04-27 00:19:34 +00:00
905c596491 Update module github.com/go-sql-driver/mysql to v1.7.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 00:03:00 +00:00
f8dcf14346 add tracking
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 00:16:18 +00:00
b031bca91f add tracking
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 00:15:35 +00:00
5938a693c9 Merge pull request 'Update module github.com/irlndts/go-discogs to v0.3.6' (#11) from renovate/github.com-irlndts-go-discogs-0.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #11
2023-04-13 00:20:00 +00:00
25bc263c62 upstream fixed their notes problem
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-12 20:18:55 -04:00
25b1ced464 Update module github.com/irlndts/go-discogs to v0.3.6
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-13 00:00:57 +00:00
91eafafd84 Merge pull request 'Update mysql Docker tag to v8' (#9) from renovate/mysql-8.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #9
2023-04-12 00:27:26 +00:00
c55c9116d0 Update mysql Docker tag to v8
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-12 00:27:08 +00:00
ef55ec0663 Merge pull request 'Update module github.com/gdamore/tcell to v2' (#8) from renovate/github.com-gdamore-tcell-2.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #8
2023-04-08 02:44:06 +00:00
0a5cea9fb1 finish upgrade
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-07 22:42:54 -04:00
2118e8b790 Update module github.com/gdamore/tcell to v2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-08 00:02:44 +00:00
1ab5a20fb2 Merge pull request 'Update golang Docker tag to v1.20' (#6) from renovate/golang-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #6
2023-04-07 02:54:16 +00:00
c1085924f7 Merge pull request 'Update module github.com/go-sql-driver/mysql to v1.7.0' (#7) from renovate/github.com-go-sql-driver-mysql-1.x into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #7
2023-04-07 02:52:21 +00:00
3736096531 Update module github.com/go-sql-driver/mysql to v1.7.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-07 01:00:58 +00:00
7dea7afd86 Update golang Docker tag to v1.20
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-07 01:00:43 +00:00
fa2f48a4a3 Add renovate.json
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-07 00:00:47 +00:00
2e3359df7a lazy-load record covers to avoid 429 responses
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-03-05 08:48:58 -05:00
db3387aac9 do the same for the records view
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-25 17:27:43 -05:00
e6e20a32f8 update two small lighthouse flags
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-25 17:26:25 -05:00
26b6ce6157 remove fontawesome in favor of raw unicode arrows
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-25 17:22:57 -05:00
8bdf848cbf finish up rewrite
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-11 23:00:38 -05:00
3b2e8cc79b unleashing flexbox upon thee 2023-02-11 22:31:46 -05:00
727e4e7867 make cover larger and transparent
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-04 21:30:18 -05:00
b11316abe5 add a book count
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-02-04 20:51:40 -05:00
f802943316 use parent docker
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-10-30 20:30:10 -04:00
20ea787617 try and force client to use tls
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-30 20:24:20 -04:00
06f9f864c4 connect on encrypted port
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-30 20:20:52 -04:00
fff582aaf0 try and disable tls
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-30 20:18:21 -04:00
b692fac091 bump docker version to try and avoid cgroups issue
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-30 20:14:14 -04:00
3fdc4bf509 remove extra spaces from notes field
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-30 20:10:43 -04:00
0e878dac97 bump to 1.18
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-05-07 15:50:14 -04:00
3e34199f3b no need to sleep, the test step takes care of that
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-05-07 15:49:37 -04:00
d45c3ebf33 correct missing quote
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-05-07 15:46:56 -04:00
e8388b979c remove onloan column
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-05-07 15:35:07 -04:00
2814c5dc68 remove vfs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-24 13:01:56 -04:00
98f4c4b0c1 add compose_token to secrets list
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-20 20:54:01 -04:00
48fc2970ad end-to-end deployment (#4)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-authored-by: David Ashby <delta.mu.alpha@gmail.com>
Reviewed-on: #4
2022-04-21 00:45:58 +00:00
f9d1cf744e refactor a bit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 17:54:59 -04:00
2346f17edd only push builds on master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 15:33:23 -04:00
886e28f348 correctly link build tag
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 15:29:40 -04:00
6a42420d36 add readme
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 15:28:37 -04:00
ce194c1418 add comment
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 15:26:33 -04:00
ca27b9f1aa secrets
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 15:19:06 -04:00
efd624bd7d try new registry location
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-03 15:07:38 -04:00
c27751dd93 test mounting socket
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-03 13:58:28 -04:00
e56b6da79d checking docker ps output
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-04-03 13:51:22 -04:00
63c334a40f add missing line break 2022-04-03 12:33:15 -04:00
a38fdaef91 hide label variant ()
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-03 12:29:24 -04:00
5f766a0efb Merge pull request 'add file system persistence of record API calls to avoid hammering the discogs API' (#3) from persist into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: #3
2022-04-03 15:51:17 +00:00
5fed609b13 add file system persistence of record API calls to avoid hammering the discogs API
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-04-03 11:26:26 -04:00
7b7e8d0058 remove tls entirely
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:37:52 -04:00
ab6de21418 try forcing client to use tls
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:35:29 -04:00
729ed30450 use tls throughout
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:32:44 -04:00
2116fbb15c no it looks like it really does have to be 2376
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:28:44 -04:00
4b78201bd1 wait for service to start
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:27:50 -04:00
ad262c5fde update port
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:21:15 -04:00
79c718153d try dind
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 20:20:34 -04:00
c492eba657 remove woodpecker until I decide how I want to do docker-in-docker 2022-04-02 20:14:50 -04:00
8a1e5f2b17 give woodpecker a try
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-04-02 19:59:31 -04:00
83b00b69b1 correct italics for album names 2022-04-02 19:53:16 -04:00
5e101c236a correct from label 2022-04-02 17:31:02 -04:00
759835993e copy cert bundle into scratch 2022-04-02 17:30:25 -04:00
04aadf1d10 add logs for discogs initial load error 2022-04-02 17:26:49 -04:00
88d9c4f2f8 Merge pull request 'discogs' (#2) from discogs into master
Reviewed-on: #2
2022-04-02 21:21:22 +00:00
781d96ca14 wrap up initial discogs work 2022-04-02 17:21:02 -04:00
8cff0ec6ab update favicon 2022-04-02 13:42:09 -04:00
832e2025a0 initial UI, pagination starts at 1 for some reason 2022-04-02 13:38:14 -04:00
474ea9b57c struct name idiomatic 2022-04-02 13:10:16 -04:00
84803f1e3d discogs basic backend 2022-03-13 18:04:09 -04:00
98584bbef6 rename book package to media 2022-03-05 10:58:15 -05:00
c9b70f02e7 bump go version 2022-01-15 15:51:06 -05:00
7ee118c1cd fix coverURL usage in the frontend 2022-01-15 15:46:59 -05:00
3e1b06e95a move actual onload trigger back into the html file 2021-10-31 20:07:41 -04:00
b52949f3e9 move js file into its own file 2021-10-31 20:00:04 -04:00
4a02014bef make the search function pure instead of calling renderTable 2021-10-31 19:51:19 -04:00
38 changed files with 2056 additions and 1874 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/server
/manager
*.properties
.DS_Store
*.csv
/vendor
.recordsCache
.config

23
.woodpecker.yml Normal file
View File

@@ -0,0 +1,23 @@
steps:
test:
image: golang:1.24
commands:
- go test ./...
build:
image: docker
commands:
- apk add curl
- docker login -u docker -p $DOCKER_PASSWORD registry.yetaga.in
- docker build -t registry.yetaga.in/library:latest .
- docker push registry.yetaga.in/library:latest
- 'curl http://100.113.98.36:4000/api/fetch -H "Authorization: Bearer $COMPOSE_TOKEN"'
- 'curl http://100.113.98.36:4000/api/update -H "Authorization: Bearer $COMPOSE_TOKEN"'
environment:
DOCKER_PASSWORD:
from_secret: docker_password
COMPOSE_TOKEN:
from_secret: compose_token
when:
branch: "master"
volumes:
- /var/run/docker.sock:/var/run/docker.sock

View File

@@ -1,8 +1,9 @@
FROM golang:1.16
FROM golang:1.24
WORKDIR /src
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/serve
FROM scratch
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=0 /src/server ./
CMD ["/server"]

View File

@@ -1,34 +1,27 @@
.PHONY: up down run-server run-manager test
.PHONY: up down run test
GOFILES=$(shell find . -name '*.go' -o -name 'go.*')
STATICFILES=$(shell find . -name '*.js' -o -name '*.css' -o -name '*.html')
SQLFILES=$(shell find . -name '*.sql')
ifneq (,$(wildcard ./local.properties))
include local.properties
export
endif
build: server manager
build: server
run-server: build
run: build
./server
run-manager: build
./manager
server: $(GOFILES) $(STATICFILES)
go build -o server ./cmd/serve
manager: $(GOFILES) $(SQLFILES)
go build -o manager ./cmd/manage
test:
go test ./... -cover
# dev dependencies
up:
docker compose up -d
docker-compose up -d
down:
docker compose down
docker-compose down

BIN
cli Executable file

Binary file not shown.

View File

@@ -1,113 +0,0 @@
package main
import (
"git.yetaga.in/alazyreader/library/book"
"github.com/gdamore/tcell"
)
// error message
type EventError struct {
tcell.EventTime
err error
}
func NewEventError(err error) *EventError {
e := &EventError{err: err}
e.SetEventNow()
return e
}
// save change to book
type EventBookUpdate struct {
tcell.EventTime
book *book.Book
}
func NewEventBookUpdate(b *book.Book) *EventBookUpdate {
e := &EventBookUpdate{book: b}
e.SetEventNow()
return e
}
func (e *EventBookUpdate) Book() *book.Book {
return e.book
}
// open new book in display
type EventLoadBook struct {
tcell.EventTime
ID int
}
func NewEventLoadBook(id int) *EventLoadBook {
e := &EventLoadBook{ID: id}
e.SetEventNow()
return e
}
// open new book in display
type EventEnterBook struct {
tcell.EventTime
}
func NewEventEnterBook() *EventEnterBook {
e := &EventEnterBook{}
e.SetEventNow()
return e
}
// switch back to menu control
type EventExitBook struct {
tcell.EventTime
}
func NewEventExitBook() *EventExitBook {
e := &EventExitBook{}
e.SetEventNow()
return e
}
// open import window
type EventOpenImport struct {
tcell.EventTime
}
func NewEventOpenImport() *EventOpenImport {
e := &EventOpenImport{}
e.SetEventNow()
return e
}
// attempt to import given filename.csv
type EventAttemptImport struct {
tcell.EventTime
filename string
}
func NewEventAttemptImport(f string) *EventAttemptImport {
e := &EventAttemptImport{filename: f}
e.SetEventNow()
return e
}
// close import window
type EventCloseImport struct {
tcell.EventTime
}
func NewEventCloseImport() *EventCloseImport {
e := &EventCloseImport{}
e.SetEventNow()
return e
}
// quit
type EventQuit struct {
tcell.EventTime
}
func NewEventQuit() *EventQuit {
e := &EventQuit{}
e.SetEventNow()
return e
}

View File

@@ -1,336 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"runtime/debug"
"strings"
"sync"
"git.yetaga.in/alazyreader/library/book"
"git.yetaga.in/alazyreader/library/config"
"git.yetaga.in/alazyreader/library/database"
"git.yetaga.in/alazyreader/library/importer"
"git.yetaga.in/alazyreader/library/ui"
"github.com/gdamore/tcell"
"github.com/kelseyhightower/envconfig"
)
// State holds the UI state keys=>value map and manages access to the map with a mutex
type State struct {
m sync.Mutex
stateMap map[string]interface{}
}
// key, present
func (s *State) Get(key string) interface{} {
s.m.Lock()
defer s.m.Unlock()
if s.stateMap == nil {
s.stateMap = make(map[string]interface{})
}
k, ok := s.stateMap[key]
if !ok {
return nil
}
return k
}
// key, value
func (s *State) Set(key string, value interface{}) {
s.m.Lock()
defer s.m.Unlock()
if s.stateMap == nil {
s.stateMap = make(map[string]interface{})
}
s.stateMap[key] = value
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// UI states
const (
IN_MENU = iota
IN_BOOK
IN_IMPORT
)
func main() {
var c config.Config
err := envconfig.Process("library", &c)
if err != nil {
log.Fatalln(err)
}
// create state
state := State{}
// set up DB connection
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
if c.DBPass != "" { // obscure password
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3))
}
log.Fatalf("vars: %+v", c)
}
lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
if err != nil {
log.Fatalln(err)
}
err = lib.PrepareDatabase(context.Background())
if err != nil {
log.Fatalln(err)
}
_, _, err = lib.RunMigrations(context.Background())
if err != nil {
log.Fatalln(err)
}
books, err := lib.GetAllBooks(context.Background())
if err != nil {
log.Fatalln(err)
}
state.Set("library", books)
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalln(err)
}
err = screen.Init()
if err != nil {
log.Fatalln(err)
}
// cleanup our screen and log if we panic and crash out somewhere
defer func() {
if r := recover(); r != nil {
if screen != nil {
screen.Fini()
}
fmt.Println("fatal panic;", r)
if c.Debug {
fmt.Println("stacktrace: \n" + string(debug.Stack()))
}
return
}
}()
// book list and options menu (left column)
l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0)
menu := ui.NewBox(
"library",
[]string{"˄˅ select", "⏎ edit", "(n)ew", "(i)mport", "(q)uit"},
ui.Contents{{
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
Container: l,
}},
ui.StyleActive,
false,
)
activeBookDetails := ui.NewBookDetails(&book.Book{})
// book display (right column)
activeBook := ui.NewBox(
"book",
[]string{"˄˅ select", "⏎ edit", "(esc) close"},
ui.Contents{{
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
Container: activeBookDetails,
}},
ui.StyleInactive,
false,
)
// parent container
container := ui.NewContainer(
ui.Contents{
{Container: menu, Offsets: ui.Offsets{Percent: 1}},
{Container: activeBook, Offsets: ui.Offsets{Percent: 2}},
},
ui.LayoutHorizontalPercent,
)
// import pop-up
wd, _ := os.Getwd()
fileSelector := ui.NewEditableTextLine(wd)
fileSelector.ResetCursor(false)
fileSelector.SetStyle(ui.StyleActive.Underline(true))
popup := ui.NewBox(
"import csv file",
[]string{"⏎ submit", "(esc)close"},
ui.Contents{
{Container: fileSelector, Offsets: ui.Offsets{Top: 2, Left: 2}},
},
ui.StyleActive,
false,
)
popup.SetVisible(false)
// error pop-up
errorMessage := ui.NewEditableTextLine("")
errorPopup := ui.NewBox(
"error",
[]string{"⏎ close"},
ui.Contents{
{Container: errorMessage, Offsets: ui.Offsets{Top: 1, Left: 1}},
},
ui.StyleActive.Bold(true).Foreground(tcell.ColorRed),
false,
)
errorPopup.SetVisible(false)
// init
screen.Clear()
w, h := screen.Size()
container.SetSize(0, 0, h, w)
container.Draw(screen)
screen.Sync()
// init UI state
state.Set("ui_state", IN_MENU)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
// UI loop
for {
e := screen.PollEvent()
switch v := e.(type) {
case *tcell.EventError:
fmt.Fprintf(os.Stderr, "%v", v)
screen.Beep()
case *tcell.EventKey: // input handling
curr := state.Get("ui_state").(int)
if curr == IN_MENU {
if v.Key() == tcell.KeyUp && l.Selected() > 0 {
l.SetSelected(l.Selected() - 1)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 {
l.SetSelected(l.Selected() + 1)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
if v.Key() == tcell.KeyEnter {
screen.PostEvent(NewEventEnterBook())
}
if v.Rune() == 'i' {
screen.PostEvent(NewEventOpenImport())
}
if v.Rune() == 'q' {
screen.PostEvent(NewEventQuit())
}
} else if curr == IN_BOOK {
if v.Key() == tcell.KeyEsc {
screen.PostEvent(NewEventExitBook())
}
} else if curr == IN_IMPORT {
if v.Key() == tcell.KeyEsc {
fileSelector.SetText(wd)
fileSelector.ResetCursor(false)
screen.PostEvent(NewEventCloseImport())
}
if v.Key() == tcell.KeyBackspace || v.Key() == tcell.KeyBackspace2 {
fileSelector.DeleteAtCursor()
} else if v.Key() == tcell.KeyRight {
fileSelector.MoveCursor(1)
} else if v.Key() == tcell.KeyLeft {
fileSelector.MoveCursor(-1)
} else if v.Key() == tcell.KeyEnter {
screen.PostEvent(NewEventAttemptImport(fileSelector.Text()))
} else if v.Rune() != 0 {
fileSelector.InsertAtCursor(v.Rune())
}
}
case *tcell.EventResize: // screen redraw
w, h := screen.Size()
container.SetSize(0, 0, h, w)
case *EventBookUpdate:
// TK
case *EventEnterBook:
activeBook.SetStyle(ui.StyleActive)
menu.SetStyle(ui.StyleInactive)
state.Set("ui_state", IN_BOOK)
case *EventExitBook:
state.Set("ui_state", IN_MENU)
activeBook.SetStyle(ui.StyleInactive)
menu.SetStyle(ui.StyleActive)
case *EventLoadBook:
activeBookDetails.SetBook(GetBookByID(v.ID, books))
case *EventOpenImport:
state.Set("ui_state", IN_IMPORT)
menu.SetStyle(ui.StyleInactive)
popup.SetVisible(true)
popup.SetSize(6, 3, 5, 80)
case *EventAttemptImport:
// this will block other events, but it shouldn't take too long...
f, err := os.Open(v.filename)
if err != nil {
screen.PostEvent(NewEventError(err))
continue
}
books, err := importer.CSVToBooks(f)
if err != nil {
screen.PostEvent(NewEventError(err))
continue
}
for b := range books {
err = lib.AddBook(context.Background(), &books[b])
if err != nil {
screen.PostEvent(NewEventError(err))
}
}
screen.PostEvent(NewEventCloseImport())
allbooks, err := lib.GetAllBooks(context.Background())
if err != nil {
screen.PostEvent(NewEventError(err))
}
state.Set("library", allbooks)
state.Set("ui_state", IN_MENU)
case *EventCloseImport:
state.Set("ui_state", IN_MENU)
screen.HideCursor()
menu.SetStyle(ui.StyleActive)
popup.SetVisible(false)
case *EventError:
errorMessage.SetText(v.err.Error())
errorPopup.SetVisible(true)
case *EventQuit:
screen.Fini()
fmt.Printf("Thank you for playing Wing Commander!\n\n")
return
case *tcell.EventInterrupt:
case *tcell.EventMouse:
case *tcell.EventTime:
default:
}
// repaint
l.SetMembers(Titles(state.Get("library").([]book.Book)))
container.Draw(screen)
popup.Draw(screen)
errorPopup.Draw(screen)
screen.Show()
}
}
func Titles(lb []book.Book) []ui.ListKeyValue {
r := []ui.ListKeyValue{}
for i := range lb {
r = append(r, ui.ListKeyValue{
Key: lb[i].ID,
Value: lb[i].Title,
})
}
return r
}
func GetBookByID(id int, lb []book.Book) *book.Book {
for i := range lb {
if lb[i].ID == id {
return &lb[i]
}
}
return &book.Book{}
}

179
cmd/serve/api.go Normal file
View File

@@ -0,0 +1,179 @@
package main
import (
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"git.yetaga.in/alazyreader/library/media"
"tailscale.com/client/tailscale"
)
type Router struct {
static fs.FS
lib Library
rcol RecordCollection
query Query
ts *tailscale.LocalClient
isAdmin bool
}
type path map[string]func()
func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if f, ok := h[r.Method]; ok {
f()
return
}
writeJSONerror(w, "method not supported", http.StatusMethodNotAllowed)
}
func writeJSONerror(w http.ResponseWriter, err string, status int) {
log.Println(err)
writeJSON(w, struct{ Status, Reason string }{Status: "error", Reason: err}, status)
}
func writeJSON(w http.ResponseWriter, b any, status int) {
bytes, err := json.Marshal(b)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
w.Write(bytes)
w.Write([]byte("\n"))
}
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/mode":
path{
http.MethodGet: func() {
writeJSON(w, struct{ Admin bool }{Admin: router.isAdmin}, http.StatusOK)
},
}.ServeHTTP(w, r)
case "/api/whoami":
if !router.isAdmin {
http.NotFoundHandler().ServeHTTP(w, r)
return
}
path{
http.MethodGet: func() { getWhoAmI(router.ts, w, r) },
}.ServeHTTP(w, r)
case "/api/records":
path{
http.MethodGet: func() { getRecords(router.rcol, w, r) },
}.ServeHTTP(w, r)
case "/api/books":
p := path{
http.MethodGet: func() { getBooks(router.lib, w, r) },
}
if router.isAdmin {
p[http.MethodPost] = func() { addBook(router.lib, w, r) }
p[http.MethodDelete] = func() { deleteBook(router.lib, w, r) }
}
p.ServeHTTP(w, r)
case "/api/query":
if !router.isAdmin {
http.NotFoundHandler().ServeHTTP(w, r)
return
}
path{
http.MethodPost: func() { lookupBook(router.query, w, r) },
}.ServeHTTP(w, r)
default:
static(router.static).ServeHTTP(w, r)
}
}
func getBooks(l Library, w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllBooks(r.Context())
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, books, http.StatusOK)
}
func addBook(l Library, w http.ResponseWriter, r *http.Request) {
book, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
if err = l.AddBook(r.Context(), book); err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
}
func deleteBook(l Library, w http.ResponseWriter, r *http.Request) {
book, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
if err = l.DeleteBook(r.Context(), book); err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
}
func getRecords(l RecordCollection, w http.ResponseWriter, r *http.Request) {
records, err := l.GetAllRecords(r.Context())
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, records, http.StatusOK)
}
func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request) {
whois, err := ts.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, whois.UserProfile, http.StatusOK)
}
func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
req, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
book, err := query.GetByISBN(req.ISBN13)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, book, http.StatusOK)
}
func static(f fs.FS) http.Handler {
return http.FileServer(http.FS(f))
}
func ReadBody[T any](r io.ReadCloser) (*T, error) {
t := new(T)
if r == nil {
return t, fmt.Errorf("no body provided")
}
defer r.Close()
b, err := io.ReadAll(r)
if err != nil {
return t, fmt.Errorf("error reading body: %w", err)
}
err = json.Unmarshal(b, t)
if err != nil {
return t, fmt.Errorf("error reading body: %w", err)
}
return t, nil
}

View File

@@ -2,99 +2,169 @@ package main
import (
"context"
"encoding/json"
"io/fs"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"time"
"git.yetaga.in/alazyreader/library/book"
"git.yetaga.in/alazyreader/library/config"
"git.yetaga.in/alazyreader/library/database"
"git.yetaga.in/alazyreader/library/frontend"
"git.yetaga.in/alazyreader/library/media"
"git.yetaga.in/alazyreader/library/query"
"github.com/kelseyhightower/envconfig"
"golang.org/x/sync/errgroup"
"tailscale.com/tsnet"
"tailscale.com/util/must"
)
func max(a, b int) int {
if a > b {
return a
}
return b
func obscureStr(in string, l int) string {
return in[0:max(l, len(in))] + strings.Repeat("*", max(0, len(in)-l))
}
type Library interface {
GetAllBooks(context.Context) ([]book.Book, error)
GetAllBooks(context.Context) ([]media.Book, error)
AddBook(context.Context, *media.Book) error
DeleteBook(context.Context, *media.Book) error
}
type Router struct {
static fs.FS
lib Library
type RecordCollection interface {
GetAllRecords(context.Context) ([]media.Record, error)
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/api" {
APIHandler(r.lib).ServeHTTP(w, req)
return
}
StaticHandler(r.static).ServeHTTP(w, req)
}
func APIHandler(l Library) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllBooks(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b, err := json.Marshal(books)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(b)
w.Write([]byte("\n"))
})
}
func StaticHandler(f fs.FS) http.Handler {
return http.FileServer(http.FS(f))
type Query interface {
GetByISBN(string) (*media.Book, error)
}
func main() {
var c config.Config
err := envconfig.Process("library", &c)
if err != nil {
log.Fatalln(err)
}
f, err := frontend.Root()
if err != nil {
log.Fatalln(err)
}
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
if c.DBPass != "" { // obscure password
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3))
must.Do(envconfig.Process("library", &c))
var lib Library
if c.DBType == "memory" {
lib = &database.Memory{}
} else if c.DBType == "sql" {
sqllib, latest, run, err := setupSQL(c)
if err != nil {
log.Fatalf("sql connection err: %v", err)
}
log.Fatalf("vars: %+v", c)
log.Printf("latest migration: %d; migrations run: %d", latest, run)
lib = sqllib
}
lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
if err != nil {
log.Fatalln(err)
}
err = lib.PrepareDatabase(context.Background())
if err != nil {
log.Fatalln(err)
}
latest, run, err := lib.RunMigrations(context.Background())
if err != nil {
log.Fatalln(err)
}
log.Printf("latest migration: %d; migrations run: %d", latest, run)
r := &Router{
static: f,
lib: lib,
}
log.Println("Listening on http://localhost:8080/")
log.Fatalln(http.ListenAndServe(":8080", r))
discogsCache := must.Get(database.NewDiscogsCache(
c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
))
queryProvider := &query.GoogleBooks{}
staticRoot := must.Get(frontend.Root())
servers := make(chan (*http.Server), 3)
errGroup := errgroup.Group{}
errGroup.Go(func() error {
return start(servers)(publicServer(8080, &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
isAdmin: false,
}))
})
errGroup.Go(func() error {
return start(servers)(tailscaleListener("library-admin", &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
query: queryProvider,
isAdmin: true,
}))
})
errGroup.Go(func() error {
return shutdown(servers)
})
log.Println(errGroup.Wait())
}
func setupSQL(c config.Config) (Library, int, int, error) {
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
if c.DBPass != "" {
c.DBPass = obscureStr(c.DBPass, 3)
}
if c.DiscogsToken != "" {
c.DiscogsToken = obscureStr(c.DiscogsToken, 3)
}
return nil, 0, 0, fmt.Errorf("invalid config; vars provided: %+v", c)
}
sql, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
if err != nil {
return nil, 0, 0, err
}
err = sql.PrepareDatabase(context.Background())
if err != nil {
return nil, 0, 0, err
}
latest, run, err := sql.RunMigrations(context.Background())
if err != nil {
return nil, 0, 0, err
}
return sql, latest, run, nil
}
func start(servers chan (*http.Server)) func(*http.Server, net.Listener, error) error {
return func(s *http.Server, l net.Listener, err error) error {
if err != nil {
return err
}
servers <- s
return s.Serve(l)
}
}
func shutdown(servers chan (*http.Server)) error {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
<-sigint
close(servers)
var err error
for server := range servers {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
if shutdownerr := server.Shutdown(ctx); shutdownerr != nil {
err = shutdownerr
}
cancel()
}
return err
}
func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, error) {
server := &http.Server{Handler: handler}
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, nil, err
}
log.Printf("public server: http://0.0.0.0:%d/", port)
return server, ln, nil
}
func tailscaleListener(hostname string, handler *Router) (*http.Server, net.Listener, error) {
s := &tsnet.Server{
Dir: ".config/" + hostname,
Hostname: hostname,
Logf: func(s string, a ...any) { // silence most tsnet logs
if strings.HasPrefix(s, "To start this tsnet server") {
log.Printf(s, a...)
}
},
}
ln, err := s.Listen("tcp", ":80")
if err != nil {
return nil, nil, err
}
handler.ts, err = s.LocalClient()
if err != nil {
return nil, nil, err
}
log.Printf("management server: http://%s/", hostname)
return &http.Server{Handler: handler}, ln, nil
}

View File

@@ -1,10 +1,15 @@
package config
type Config struct {
DBUser string
DBPass string
DBHost string
DBPort string
DBName string
Debug bool
DBType string `default:"sql"`
DBUser string
DBPass string
DBHost string
DBPort string
DBName string
DiscogsToken string
DiscogsUser string
DiscogsPersist bool
DiscogsCacheFile string `default:".recordsCache"`
Debug bool
}

View File

@@ -5,43 +5,43 @@ import (
"fmt"
"sync"
"git.yetaga.in/alazyreader/library/book"
"git.yetaga.in/alazyreader/library/media"
)
type Memory struct {
lock sync.Mutex
shelf []book.Book
shelf []media.Book
}
func (m *Memory) GetAllBooks(_ context.Context) ([]book.Book, error) {
func (m *Memory) GetAllBooks(_ context.Context) ([]media.Book, error) {
m.lock.Lock()
defer m.lock.Unlock()
if m.shelf == nil {
m.shelf = []book.Book{}
m.shelf = []media.Book{}
}
return m.shelf, nil
}
func (m *Memory) AddBook(_ context.Context, b *book.Book) error {
func (m *Memory) AddBook(_ context.Context, b *media.Book) error {
m.lock.Lock()
defer m.lock.Unlock()
if m.shelf == nil {
m.shelf = []book.Book{}
m.shelf = []media.Book{}
}
m.shelf = append(m.shelf, *b)
return nil
}
func (m *Memory) UpdateBook(_ context.Context, old, new *book.Book) error {
func (m *Memory) UpdateBook(_ context.Context, old, new *media.Book) error {
m.lock.Lock()
defer m.lock.Unlock()
if m.shelf == nil {
m.shelf = []book.Book{}
m.shelf = []media.Book{}
return fmt.Errorf("book does not exist")
}
@@ -58,12 +58,12 @@ func (m *Memory) UpdateBook(_ context.Context, old, new *book.Book) error {
return fmt.Errorf("book does not exist")
}
func (m *Memory) DeleteBook(_ context.Context, b *book.Book) error {
func (m *Memory) DeleteBook(_ context.Context, b *media.Book) error {
m.lock.Lock()
defer m.lock.Unlock()
if m.shelf == nil {
m.shelf = []book.Book{}
m.shelf = []media.Book{}
return fmt.Errorf("book does not exist")
}

View File

@@ -0,0 +1 @@
ALTER TABLE books DROP COLUMN onloan;

View File

@@ -0,0 +1,2 @@
ALTER TABLE `books`
ADD COLUMN `childrens` tinyint(1) NOT NULL DEFAULT 0

View File

@@ -10,7 +10,7 @@ import (
"strings"
"time"
"git.yetaga.in/alazyreader/library/book"
"git.yetaga.in/alazyreader/library/media"
_ "github.com/go-sql-driver/mysql"
)
@@ -49,15 +49,15 @@ func (m *MySQL) PrepareDatabase(ctx context.Context) error {
return fmt.Errorf("uninitialized mysql client")
}
tablecheck := `SELECT count(*) AS count
tablecheck := fmt.Sprintf(`SELECT count(*) AS count
FROM information_schema.TABLES
WHERE TABLE_NAME = '` + m.versionTable + `'
AND TABLE_SCHEMA in (SELECT DATABASE());`
tableschema := `CREATE TABLE ` + m.versionTable + `(
WHERE TABLE_NAME = '%s'
AND TABLE_SCHEMA in (SELECT DATABASE());`, m.versionTable)
tableschema := fmt.Sprintf(`CREATE TABLE %s (
id INT NOT NULL,
name VARCHAR(100) NOT NULL,
datetime DATE,
PRIMARY KEY (id))`
PRIMARY KEY (id))`, m.versionTable)
var versionTableExists int
m.connection.QueryRowContext(ctx, tablecheck).Scan(&versionTableExists)
@@ -73,8 +73,9 @@ func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) {
return 0, fmt.Errorf("uninitialized mysql client")
}
migrationCheck := fmt.Sprintf("SELECT COALESCE(MAX(id), 0) FROM %s", m.versionTable)
var latestMigration int
err := m.connection.QueryRowContext(ctx, "SELECT COALESCE(MAX(id), 0) FROM "+m.versionTable).Scan(&latestMigration)
err := m.connection.QueryRowContext(ctx, migrationCheck).Scan(&latestMigration)
return latestMigration, err
}
@@ -97,6 +98,9 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
}
mig.id, mig.name = id, name
mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name())
if err != nil {
return 0, 0, fmt.Errorf("failure loading migration: %w", err)
}
migrations[mig.id] = mig
}
}
@@ -116,6 +120,7 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
if err != nil {
return latestMigrationRan, 0, err
}
migrationLogSql := fmt.Sprintf("INSERT INTO %s (id, name, datetime) VALUES (?, ?, ?)", m.versionTable)
migrationsRun := 0
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
mig := migrations[latestMigrationRan+1]
@@ -127,7 +132,7 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
}
return latestMigrationRan, migrationsRun, err
}
_, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now())
_, err = tx.ExecContext(ctx, migrationLogSql, mig.id, mig.name, time.Now())
if err != nil {
nestederr := tx.Rollback()
if nestederr != nil {
@@ -142,82 +147,55 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
return latestMigrationRan, migrationsRun, err
}
func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) {
func (m *MySQL) GetAllBooks(ctx context.Context) ([]media.Book, error) {
if m.connection == nil {
return nil, fmt.Errorf("uninitialized mysql client")
}
books := []book.Book{}
rows, err := m.connection.QueryContext(ctx, `
SELECT id,
title,
authors,
sortauthor,
isbn10,
isbn13,
format,
genre,
publisher,
series,
volume,
year,
signed,
description,
notes,
onloan,
coverurl
FROM `+m.tableName)
allBooksQuery := fmt.Sprintf(`SELECT
id, title, authors, sortauthor, isbn10, isbn13, format, genre, publisher,
series, volume, year, signed, description, notes, coverurl, childrens
FROM %s`, m.tableName)
books := []media.Book{}
rows, err := m.connection.QueryContext(ctx, allBooksQuery)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
b := book.Book{}
b := media.Book{}
var authors string
err := rows.Scan(
&b.ID, &b.Title, &authors,
&b.SortAuthor, &b.ISBN10, &b.ISBN13,
&b.Format, &b.Genre, &b.Publisher,
&b.Series, &b.Volume, &b.Year,
&b.Signed, &b.Description, &b.Notes,
&b.OnLoan, &b.CoverURL)
&b.ID, &b.Title, &authors, &b.SortAuthor, &b.ISBN10, &b.ISBN13, &b.Format, &b.Genre, &b.Publisher,
&b.Series, &b.Volume, &b.Year, &b.Signed, &b.Description, &b.Notes, &b.CoverURL, &b.Childrens)
if err != nil {
return nil, err
}
b.Authors = strings.Split(authors, ";")
b.Notes = strings.TrimSpace(b.Notes)
books = append(books, b)
}
return books, nil
}
func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error {
func (m *MySQL) AddBook(ctx context.Context, b *media.Book) error {
if m.connection == nil {
return fmt.Errorf("uninitialized mysql client")
}
res, err := m.connection.ExecContext(ctx, `
INSERT INTO `+m.tableName+`
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl)
(
title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series,
volume, year, signed, description, notes, coverurl, childrens
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
b.Title,
strings.Join(b.Authors, ";"),
b.SortAuthor,
b.ISBN10,
b.ISBN13,
b.Format,
b.Genre,
b.Publisher,
b.Series,
b.Volume,
b.Year,
b.Signed,
b.Description,
b.Notes,
b.OnLoan,
b.CoverURL,
b.Title, strings.Join(b.Authors, ";"), b.SortAuthor, b.ISBN10, b.ISBN13, b.Format, b.Genre, b.Publisher, b.Series,
b.Volume, b.Year, b.Signed, b.Description, b.Notes, b.CoverURL, b.Childrens,
)
if err != nil {
return err
@@ -232,7 +210,7 @@ func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error {
return nil
}
func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
func (m *MySQL) UpdateBook(ctx context.Context, old, new *media.Book) error {
if m.connection == nil {
return fmt.Errorf("uninitialized mysql client")
}
@@ -242,41 +220,13 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
res, err := m.connection.ExecContext(ctx, `
UPDATE `+m.tableName+`
SET id=?
title=?
authors=?
sortauthor=?
isbn10=?
isbn13=?
format=?
genre=?
publisher=?
series=?
volume=?
year=?
signed=?
description=?
notes=?
onloan=?
coverurl=?
SET
id=? title=? authors=? sortauthor=? isbn10=? isbn13=? format=? genre=? publisher=?
series=? volume=? year=? signed=? description=? notes=? coverurl=? childrens=?
WHERE id=?`,
new.Title,
strings.Join(new.Authors, ";"),
new.SortAuthor,
new.ISBN10,
new.ISBN13,
new.Format,
new.Genre,
new.Publisher,
new.Series,
new.Volume,
new.Year,
new.Signed,
new.Description,
new.Notes,
new.OnLoan,
new.CoverURL,
old.ID)
new.Title, strings.Join(new.Authors, ";"), new.SortAuthor, new.ISBN10, new.ISBN13, new.Format, new.Genre, new.Publisher,
new.Series, new.Volume, new.Year, new.Signed, new.Description, new.Notes, new.CoverURL, new.Childrens, old.ID,
)
if err != nil {
return err
}
@@ -290,6 +240,10 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
return nil
}
func (m *MySQL) DeleteBook(_ context.Context, b *media.Book) error {
return nil
}
func parseMigrationFileName(filename string) (int, string, error) {
sp := strings.SplitN(filename, "-", 2)
i, err := strconv.Atoi(sp[0])

182
database/records.go Normal file
View File

@@ -0,0 +1,182 @@
package database
import (
"context"
"encoding/gob"
"errors"
"fmt"
"log"
"os"
"strconv"
"sync"
"time"
"git.yetaga.in/alazyreader/library/media"
"github.com/irlndts/go-discogs"
)
type DiscogsCache struct {
authToken string
m sync.Mutex
cache []media.Record
maxCacheAge time.Duration
lastRefresh time.Time
client discogs.Discogs
username string
persistence bool
persistFile string
}
type persistence struct {
CachedRecordSlice []media.Record
LastRefresh time.Time
}
func NewDiscogsCache(token string, maxCacheAge time.Duration, username string, persist bool, persistFile string) (*DiscogsCache, error) {
client, err := discogs.New(&discogs.Options{
UserAgent: "library.yetaga.in personal collection cache",
Token: token,
})
if err != nil {
return nil, err
}
cache := &DiscogsCache{
authToken: token,
client: client,
maxCacheAge: maxCacheAge,
username: username,
persistence: persist,
persistFile: persistFile,
}
if cache.persistence && cache.persistFile != "" {
cache.cache, cache.lastRefresh, err = cache.loadRecordsFromFS(context.Background())
if err != nil {
return nil, fmt.Errorf("cache load failed: %w", err)
}
if time.Now().After(cache.lastRefresh.Add(cache.maxCacheAge)) {
log.Printf("cache expired, running refresh...")
go func() {
err := cache.FlushCache(context.Background())
if err != nil {
log.Printf("error loading discogs content: %v", err)
}
}()
}
}
return cache, nil
}
func (c *DiscogsCache) FlushCache(ctx context.Context) error {
c.m.Lock()
defer c.m.Unlock()
records, err := c.fetchRecords(ctx, nil)
if err != nil {
return err
}
return c.saveRecordsToCache(ctx, records)
}
func (c *DiscogsCache) GetAllRecords(ctx context.Context) ([]media.Record, error) {
c.m.Lock()
defer c.m.Unlock()
if time.Now().After(c.lastRefresh.Add(c.maxCacheAge)) {
records, err := c.fetchRecords(ctx, nil)
if err != nil {
return c.cache, err
}
err = c.saveRecordsToCache(ctx, records)
return c.cache, err
}
return c.cache, nil
}
func (c *DiscogsCache) loadRecordsFromFS(ctx context.Context) ([]media.Record, time.Time, error) {
p := &persistence{}
f, err := os.Open(c.persistFile)
if errors.Is(err, os.ErrNotExist) {
log.Printf("%s not found, skipping file load...", c.persistFile)
return nil, time.Time{}, nil
}
if err != nil {
return nil, time.Time{}, fmt.Errorf("error opening cache file %s: %w", c.persistFile, err)
}
err = gob.NewDecoder(f).Decode(p)
if err != nil {
return nil, time.Time{}, fmt.Errorf("error readhing from cache file %s: %w", c.persistFile, err)
}
log.Printf("loaded %d records from %s", len(p.CachedRecordSlice), c.persistFile)
return p.CachedRecordSlice, p.LastRefresh, nil
}
func (c *DiscogsCache) saveRecordsToCache(ctx context.Context, records []media.Record) error {
c.cache = records
c.lastRefresh = time.Now()
if c.persistence && c.persistFile != "" {
p := persistence{
CachedRecordSlice: c.cache,
LastRefresh: c.lastRefresh,
}
f, err := os.OpenFile(c.persistFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("error opening cache file %s: %w", c.persistFile, err)
}
err = gob.NewEncoder(f).Encode(p)
if err != nil {
return fmt.Errorf("error writing to cache file %s: %w", c.persistFile, err)
}
log.Printf("wrote %d records to %s", len(c.cache), c.persistFile)
}
return nil
}
func (c *DiscogsCache) fetchRecords(ctx context.Context, pagination *discogs.Pagination) ([]media.Record, error) {
records := []media.Record{}
if pagination == nil {
pagination = getPagination(1)
}
log.Printf("calling discogs API, page %v", pagination.Page)
coll, err := c.client.CollectionItemsByFolder(c.username, 0, pagination)
if err != nil {
return records, fmt.Errorf("error loading collection: %w", err)
}
log.Printf("length: %v, first item in list: %s", len(coll.Items), coll.Items[0].BasicInformation.Title)
for i := range coll.Items {
records = append(records, collectionItemToRecord(&coll.Items[i]))
}
// recurse down the list
if coll.Pagination.URLs.Next != "" {
coll, err := c.fetchRecords(ctx, getPagination(pagination.Page+1))
if err != nil {
return records, err
}
records = append(records, coll...)
}
return records, nil
}
func getPagination(page int) *discogs.Pagination {
return &discogs.Pagination{Page: page, Sort: "added", SortOrder: "asc", PerPage: 100}
}
func collectionItemToRecord(item *discogs.CollectionItemSource) media.Record {
artists := []string{}
for _, artist := range item.BasicInformation.Artists {
artists = append(artists, artist.Name)
}
year := strconv.Itoa(item.BasicInformation.Year)
if year == "0" {
year = ""
}
return media.Record{
ID: item.ID,
AlbumName: item.BasicInformation.Title,
Artists: artists,
Identifier: item.BasicInformation.Labels[0].Catno,
Format: item.BasicInformation.Formats[0].Name,
Genre: item.BasicInformation.Genres[0],
Label: item.BasicInformation.Labels[0].Name,
Year: year,
CoverURL: item.BasicInformation.CoverImage,
DiscogsURL: fmt.Sprintf("https://www.discogs.com/release/%v", item.ID),
}
}

View File

@@ -1,7 +1,7 @@
version: "3.8"
services:
mysql:
image: mysql:5.7
image: mysql:9.3
ports:
- 3306:3306
environment:

322
frontend/files/app.js Normal file
View File

@@ -0,0 +1,322 @@
var sortState = {
sortBy: "sortAuthor",
sortOrder: "asc",
};
var admin = false;
var books;
function checkAdminMode() {
fetch("/api/mode")
.then((response) => response.json())
.then((resp) => (admin = resp.Admin))
.then(() => {
if (admin) {
var element = document.getElementById("addBook");
element.addEventListener("click", (e) => {
e.preventDefault();
renderAddBookView();
});
element.classList.remove("hidden");
}
});
}
function loadBookList() {
fetch("/api/books")
.then((response) => response.json())
.then((list) => {
// prepare response
list.forEach(apiResponseParsing);
books = list;
document.getElementById("search").addEventListener("input", rerender);
document.getElementById("childrens").addEventListener("change", rerender);
rerender();
});
}
function rerender() {
var searchValue = document.getElementById("search").value;
var childrens = document.getElementById("childrens").checked;
renderTable(search(searchValue, childrens));
}
function init() {
checkAdminMode();
loadBookList();
}
function renderAddBookView() {
document.getElementById("current").innerHTML = AddBookTemplate();
document.getElementById("lookup").addEventListener("click", (e) => {
e.preventDefault();
if (document.getElementById("isbn-13").value.length === 13) {
getPossibleBooks(document.getElementById("isbn-13").value);
} else {
console.log("no isbn");
}
});
document.getElementById("save").addEventListener("click", (e) => {
e.preventDefault();
saveBook({
title: document.getElementById("title").value,
authors: document.getElementById("authors").value.split(";"),
sortAuthor: document.getElementById("sortAuthor").value,
"isbn-10": document.getElementById("isbn-10").value,
"isbn-13": document.getElementById("isbn-13").value,
publisher: document.getElementById("publisher").value,
format: document.getElementById("format").value,
genre: document.getElementById("genre").value,
series: document.getElementById("series").value,
volume: document.getElementById("volume").value,
year: document.getElementById("year").value,
coverURL: document.getElementById("coverURL").value,
});
});
}
function getPossibleBooks(isbn) {
fetch("/api/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ "isbn-13": isbn }),
})
.then((response) => response.json())
.then((json) => {
Object.keys(json).forEach((key) => {
var elem = document.getElementById(key);
if (elem !== null) {
elem.value = json[key];
}
});
});
}
function saveBook(book) {
fetch("/api/books", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(book),
}).then(() => {
clearAddBookForm();
loadBookList();
});
}
function renderTable(bookList, sortField) {
if (sortField) {
sortState.sortOrder =
sortState.sortBy === sortField && sortState.sortOrder === "asc"
? "desc"
: "asc";
sortState.sortBy = sortField;
}
bookList.sort((one, two) =>
(one[sortState.sortBy] + one["sortTitle"]).localeCompare(
two[sortState.sortBy] + two["sortTitle"]
)
);
if (sortState.sortOrder === "desc") {
bookList.reverse();
}
bookList.forEach((e, i) => (e.rowNumber = i)); // re-key
// rendering
var bookElement = document.getElementById("books");
bookElement.innerHTML = TableTemplate(bookList);
document.getElementById("bookCount").innerHTML = `${bookList.length} books`;
// add listeners for selecting book to view
Array.from(bookElement.querySelectorAll("tbody tr"))
.slice(1) // remove header from Array
.forEach((row) => {
row.addEventListener("click", (e) => {
// add listener to swap current book
document.getElementById("current").innerHTML = BookTemplate(
bookList[e.currentTarget.id]
);
});
});
// add sorting callbacks
Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach(
(row) => {
row.addEventListener("click", function (e) {
// only add callback when there's a sortBy attribute
renderTable(bookList, e.target.dataset.sortBy);
});
}
);
// mark currently active column
bookElement
.querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
.classList.add(sortState.sortOrder);
}
function apiResponseParsing(book) {
book.sortTitle = titleCleaner(book.title);
if (!book["isbn-10"] && book["isbn-13"]) {
book["isbn-10"] = ISBNfromEAN(book["isbn-13"]);
}
if (!book.coverURL && book["isbn-10"]) {
book.coverURL =
`https://images-na.ssl-images-amazon.com/images/P/` +
book["isbn-10"] +
`.01.LZZ.jpg`;
}
return book;
}
function search(searchBy, includeChildrensBooks) {
searchBy = searchCleaner(searchBy);
return books.filter(
({ title, authors, genre, publisher, series, year, childrens }) => {
var inSearch = true;
if (searchBy !== "") {
inSearch = Object.values({
title,
authors: authors.join(" "),
genre,
publisher,
series,
year,
}).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
}
if (!includeChildrensBooks) {
return inSearch && !childrens;
}
return inSearch;
}
);
}
function titleCleaner(title) {
return title
.replace('"', "")
.replace(":", "")
.replace(/^(An?|The)\s/i, "");
}
function searchCleaner(str) {
return str
.toLowerCase()
.replaceAll('"', "")
.replaceAll(":", "")
.replaceAll("'", "")
.replaceAll(" ", "");
}
function ISBNfromEAN(EAN) {
ISBN = EAN.slice(3, 12);
var checkdigit =
(11 - (ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) % 11;
return ISBN + (checkdigit === 10 ? "X" : checkdigit);
}
function clearAddBookForm() {
document
.getElementById("newBookForm")
.childNodes.forEach((node) =>
node.nodeName === "LABEL" ? (node.lastChild.value = "") : null
);
}
function BookTemplate({
"isbn-13": isbn13,
"isbn-10": isbn10,
authors,
coverURL,
format,
publisher,
series,
signed,
title,
volume,
year,
}) {
return `<img ${coverURL ? `src="${coverURL}"` : ``}/>
<div class="bookDetails">
<h1>${title}</h1>
<h2>${authors}</h2>
<span>${[isbn10, isbn13].filter((v) => v != "").join(" / ")}</span><br/>
<span>${publisher}, ${year}</span><br/>
${
series
? `<span>${series}${volume ? `, Volume ${volume}` : ""}</span><br/>`
: ""
}
${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
<span>${format}</span>
${admin ? `<a href="#">Edit Book</a>` : ""}
</div>`;
}
function TableRowTemplate({
"isbn-13": isbn13,
"isbn-10": isbn10,
authors,
publisher,
rowNumber,
signed,
title,
year,
}) {
return `<tr class="tRow" id="${rowNumber}">
<td class="title">
${title} ${
signed ? '<span class="signed" title="Signed by the author" >✒</span>' : ""
}
</td>
<td class="author">${authors}</td>
<td class="publisher">${publisher}</td>
<td class="year">${year}</td>
<td class="isbn">${isbn13 ? isbn13 : isbn10}</td>
</tr>`;
}
function TableTemplate(books) {
return `<table class="bookTable">
<tr>
<th data-sort-by="sortTitle" class="tHeader title">Title</th>
<th data-sort-by="sortAuthor" class="tHeader author">Author</th>
<th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
<th data-sort-by="year" class="tHeader year">Year</th>
<th class="tHeader isbn">ISBN</th>
</tr>${books.reduce((acc, book) => {
return acc.concat(TableRowTemplate(book));
}, "")} </table>`;
}
function AddBookTemplate() {
return `<div class="addBookView">
<div id="newBookForm">
${[
{ name: "Title", id: "title", type: "text" },
{ name: "Authors", id: "authors", type: "text" },
{ name: "SortAuthor", id: "sortAuthor", type: "text" },
{ name: "ISBN10", id: "isbn-10", type: "text" },
{ name: "ISBN13", id: "isbn-13", type: "text" },
{ name: "Publisher", id: "publisher", type: "text" },
{ name: "Format", id: "format", type: "text" },
{ name: "Genre", id: "genre", type: "text" },
{ name: "Series", id: "series", type: "text" },
{ name: "Volume", id: "volume", type: "text" },
{ name: "Year", id: "year", type: "text" },
{ name: "CoverURL", id: "coverURL", type: "text" },
{ name: "Signed", id: "signed", type: "checkbox" },
{ name: "Childrens", id: "childrens", type: "checkbox" },
].reduce((acc, field) => {
return acc.concat(
`<label>${field.name} <input
type="${field.type}"
name="${field.name.toLowerCase()}"
id="${field.id}"
/></label><br/>`
);
}, "")}
<input id="lookup" type="submit" value="look up">
<input id="save" type="submit" value="save">
</div>
</div>`;
}

View File

@@ -1,11 +1,7 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Library</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link rel="stylesheet" href="style.css" />
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link
@@ -13,220 +9,35 @@
as="style"
rel="stylesheet preload prefetch"
/>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
var sortState = {
sortBy: "sortAuthor",
sortOrder: "asc",
};
function init() {
fetch("/api")
.then((response) => response.json())
.then((books) => {
// prepare response
books.forEach((book) => {
book.sortTitle = titleCleaner(book.title);
if (!book["isbn-10"] && book["isbn-13"]) {
book["isbn-10"] = ISBNfromEAN(book["isbn-13"]);
}
if (!book.coverurl && book["isbn-10"]) {
book.coverurl =
`https://images-na.ssl-images-amazon.com/images/P/` +
book["isbn-10"] +
`.01.LZZ.jpg`;
}
});
return books;
})
.then((books) => {
document.getElementById("search").addEventListener("input", (e) => {
search(books, e.target.value);
});
return books;
})
.then(renderTable);
}
function search(books, searchBy) {
searchBy = searchCleaner(searchBy);
if (searchBy !== "") {
books = books.filter(
({ title, authors, genre, publisher, series, year }) => {
return Object.values({
title,
authors: authors.join(" "),
genre,
publisher,
series,
year,
}).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
}
);
}
renderTable(books);
}
function renderTable(books, sortField) {
if (sortField) {
if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
sortState.sortOrder = "desc";
} else {
sortState.sortOrder = "asc";
}
sortState.sortBy = sortField;
}
books.sort((one, two) =>
(one[sortState.sortBy] + one["sortTitle"]).localeCompare(
two[sortState.sortBy] + two["sortTitle"]
)
);
if (sortState.sortOrder === "desc") {
books.reverse();
}
books.forEach((e, i) => (e.rowNumber = i)); // re-key
// rendering
var bookElement = document.getElementById("books");
bookElement.innerHTML = TableTemplate(books);
// add listeners for selecting book to view
Array.from(bookElement.querySelectorAll("tbody tr"))
.slice(1) // remove header from Array
.forEach((row) => {
row.addEventListener("click", (e) => {
// add listener to swap current book
document.getElementById("current").innerHTML = BookTemplate(
books[e.currentTarget.id]
);
});
});
// add sorting callbacks
Array.from(
bookElement.querySelectorAll("tbody tr th[data-sort-by]")
).forEach((row) => {
row.addEventListener("click", function (e) {
renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute
});
});
// mark currently active column
bookElement
.querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
.classList.add(sortState.sortOrder);
}
function titleCleaner(title) {
return title
.replace('"', "")
.replace(":", "")
.replace(/^(An?|The)\s/i, "");
}
function searchCleaner(str) {
return str
.toLowerCase()
.replaceAll('"', "")
.replaceAll(":", "")
.replaceAll("'", "")
.replaceAll(" ", "");
}
function ISBNfromEAN(EAN) {
ISBN = EAN.slice(3, 12);
var checkdigit =
(11 -
(ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) %
11;
return ISBN + (checkdigit === 10 ? "X" : checkdigit);
}
function BookTemplate({
"isbn-13": isbn13,
authors,
coverurl,
description,
format,
notes,
onLoan,
publisher,
series,
signed,
title,
volume,
year,
}) {
return `${coverurl ? `<img src="${coverurl}"/>` : ""}
<h1 ${onLoan ? "class='onLoan' " : ""}>${title}</h1>
<h2>${authors}</h2>
<span>${isbn13}</span><br/>
<span>${publisher}, ${year}</span><br/>
${
series
? `<span>${series}${volume ? `, Volume ${volume}` : ""}</span><br/>`
: ""
}
${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
<span>${format}</span>
${onLoan ? `<h2 class="onLoan">On loan to ${onLoan}</h2>` : ""}
<div class="description">
<p>${description}</p>
${notes ? `<span>Notes:</span><p>${notes}</p>` : ""}
</div>`;
}
function TableRowTemplate({
"isbn-13": isbn13,
authors,
onLoan,
publisher,
rowNumber,
signed,
title,
year,
}) {
return `<tr class="tRow ${onLoan ? "onLoan" : ""}" id="${rowNumber}">
<td class="title">
${title} ${
signed
? '<span class="signed" title="Signed by the author" >✒</span>'
: ""
}
</td>
<td class="author">${authors}</td>
<td class="publisher">${publisher}</td>
<td class="year">${year}</td>
<td class="isbn">${isbn13}</td>
</tr>`;
}
function TableTemplate(books) {
return `<table class="bookTable">
<tr>
<th data-sort-by="sortTitle" class="tHeader title">Title</th>
<th data-sort-by="sortAuthor" class="tHeader author">Author</th>
<th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
<th data-sort-by="year" class="tHeader year">Year</th>
<th class="tHeader isbn">ISBN</th>
</tr>${books.reduce((acc, book) => {
return acc.concat(TableRowTemplate(book));
}, "")} </table>`;
}
window.addEventListener("DOMContentLoaded", () => {
init();
});
window.addEventListener("DOMContentLoaded", init);
</script>
<script
defer
data-domain="library.yetaga.in"
src="https://stats.yetaga.in/js/script.js"
></script>
<meta name="description" content="A personal library record." />
</head>
<body>
<div class="wrapper">
<div id="header">
<h1>Library</h1>
<a href="/records">records</a>
<a
target="_blank"
rel="noreferrer"
href="https://git.yetaga.in/alazyreader/library"
>git</a
>
<a href="#" id="addBook" class="hidden">add book</a>
<div id="searchBox">
<label for="childrens" class="bookCount"
>Include Childrens Books?</label
>
<input id="childrens" type="checkbox" name="childrens" />
<span id="bookCount" class="bookCount">_ books</span>
<input
id="search"
type="text"

View File

@@ -0,0 +1,168 @@
var sortState = {
sortBy: "sortArtist",
sortOrder: "asc",
};
function init() {
fetch("/api/records")
.then((response) => response.json())
.then((records) => {
// prepare response
records.forEach(apiResponseParsing);
document.getElementById("search").addEventListener("input", (e) => {
renderTable(search(records, e.target.value));
});
renderTable(records);
});
}
function renderTable(records, sortField) {
if (sortField) {
if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
sortState.sortOrder = "desc";
} else {
sortState.sortOrder = "asc";
}
sortState.sortBy = sortField;
}
records.sort((one, two) =>
(one[sortState.sortBy] + one["sortName"]).localeCompare(
two[sortState.sortBy] + two["sortName"]
)
);
if (sortState.sortOrder === "desc") {
records.reverse();
}
records.forEach((e, i) => (e.rowNumber = i)); // re-key
// rendering
var recordElement = document.getElementById("records");
recordElement.innerHTML = TableTemplate(records);
var recordCount = document.getElementById("recordCount");
recordCount.innerHTML = `${records.length} records`;
// add listeners for selecting record to view
Array.from(recordElement.querySelectorAll("tbody tr"))
.slice(1) // remove header from Array
.forEach((row) => {
row.addEventListener("click", (e) => {
// add listener to swap current record
document.getElementById("current").innerHTML = RecordTemplate(
records[e.currentTarget.id]
);
});
});
// add sorting callbacks
Array.from(
recordElement.querySelectorAll("tbody tr th[data-sort-by]")
).forEach((row) => {
row.addEventListener("click", function (e) {
renderTable(records, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute
});
});
// mark currently active column
recordElement
.querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
.classList.add(sortState.sortOrder);
}
function apiResponseParsing(record) {
record.sortName = titleCleaner(record.name);
record.artists = record.artists.map((artist) => {
return artist.replace(/ \([0-9]+\)$/, "");
});
record.label = record.label.replace(/ \([0-9]+\)$/, "");
record.sortArtist = record.artists.reduce((acc, curr) => {
return (
acc +
curr
.replace(/^(An?|The)\s/i, "")
.toLowerCase()
.replaceAll('"', "")
.replaceAll(":", "")
.replaceAll("'", "")
.replaceAll(" ", "")
);
}, "");
return record;
}
function search(records, searchBy) {
searchBy = searchCleaner(searchBy);
if (searchBy !== "") {
records = records.filter(({ name, artists, genre, label, year }) => {
return Object.values({
name,
artists: artists.join(" "),
genre,
label,
year,
}).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
});
}
return records;
}
function titleCleaner(title) {
return title
.replace('"', "")
.replace(":", "")
.replace(/^(An?|The)\s/i, "");
}
function searchCleaner(str) {
return str
.toLowerCase()
.replaceAll('"', "")
.replaceAll(":", "")
.replaceAll("'", "")
.replaceAll(" ", "");
}
function RecordTemplate({
name,
artists,
coverURL,
format,
genre,
identifier,
label,
year,
discogsURL,
}) {
return `${coverURL ? `<img src="${coverURL}" loading="lazy"/>` : ""}
<h1>${name}</h1>
<h2>${artists.join(", ")}</h2>
<span>${identifier}</span><br/>
<span>${genre}, ${label}, ${year}</span><br/>
<span>${format}</span><br/>
<span>
<a
target="_blank"
href="${discogsURL}"
>
Data provided by Discogs.
</a>
</span>`;
}
function TableRowTemplate({ name, coverURL, discogsURL }) {
return `<div class="record">
<img class="cover" src="${coverURL}" loading="lazy"/>
<span class="name">${name}</span>
<a
target="_blank"
href="${discogsURL}"
class="discogsLink"
>
Data provided by Discogs.
</a>
</div>`;
}
function TableTemplate(records) {
return `<div class="flow">${records.reduce((acc, record) => {
return acc.concat(TableRowTemplate(record));
}, "")} </div>`;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Library</title>
<link rel="stylesheet" href="style.css" />
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link
href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700&display=swap"
as="style"
rel="stylesheet preload prefetch"
/>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", init);
</script>
<script defer data-domain="library.yetaga.in" src="https://stats.yetaga.in/js/script.js"></script>
<meta
name="description"
content="A scrollable view of all of my records."
/>
</head>
<body>
<div class="wrapper">
<div id="header">
<h1>Records</h1>
<a href="/">books</a>
<a
target="_blank"
rel="noreferrer"
href="https://git.yetaga.in/alazyreader/library"
>git</a
>
<div id="searchBox">
<span id="recordCount" class="recordCount">_ records</span>
<input
id="search"
type="text"
name="search"
placeholder="Search..."
/>
</div>
</div>
<div id="records"></div>
<footer>
This application uses Discogs API but is not affiliated with, sponsored
or endorsed by Discogs. Discogs is a trademark of Zink Media, LLC.
</footer>
<!-- Table goes here -->
</div>
</body>
</html>

View File

@@ -0,0 +1,238 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* site CSS starts here */
body {
overflow: hidden;
}
#header {
height: 30px;
width: calc(100vw - 20px);
padding: 4px 10px;
background-color: #f7f3dc;
border-bottom: 2px solid #d8d0a0;
font-family: "Libre Baskerville", sans-serif;
}
#header h1 {
font-size: xx-large;
display: inline;
}
#header .recordCount {
font-size: small;
color: #a29c77;
}
#searchBox {
position: absolute;
right: 10px;
top: 7px;
text-align: right;
width: 400px;
}
#searchBox input {
width: 300px;
font-size: 16px;
background: #f9f8ed;
padding: 2px 5px;
border: none;
border-bottom: 2px solid #d8d0a0;
font-family: "Libre Baskerville", sans-serif;
}
#searchBox input:focus {
outline: none;
}
#searchBox input::placeholder {
font-family: "Libre Baskerville", sans-serif;
color: #d8d0a0;
}
#records .flow {
height: calc(100vh - 35px - 15px - 20px);
padding-top: 5px;
padding-left: 20px;
padding-right: 20px;
display: flex;
justify-content: space-evenly;
align-items: flex-start;
flex-wrap: wrap;
overflow: auto;
}
#records .flow .record {
display: inline-block;
width: 250px;
padding: 15px;
border-radius: 3px;
}
#records .flow .record:nth-child(odd) {
background: #f9f8ed;
}
#records .flow .record .cover {
border-radius: 3px;
max-width: 250px;
display: block;
margin: 0 auto 3px;
overflow: unset;
}
#records .flow .record .name {
font-style: italic;
}
#records .flow .record a.discogsLink {
display: block;
text-align: right;
font-size: smaller;
padding-top: 10px;
color: #a29c77;
}
footer {
background-color: #f7f3dc;
font-size: smaller;
text-align: center;
vertical-align: bottom;
padding: 5px 0px;
position: absolute;
bottom: 0;
padding-left: 20px;
padding-right: 20px;
width: calc(100% - 40px);
color: #a29c77;
border-top: 2px solid #d8d0a0;
}

View File

@@ -133,6 +133,10 @@ body {
overflow: hidden;
}
.hidden {
display: none;
}
#header {
height: 30px;
width: calc(100vw - 20px);
@@ -147,15 +151,20 @@ body {
display: inline;
}
#header .bookCount {
font-size: small;
color: #a29c77;
}
#searchBox {
position: absolute;
right: 10px;
top: 7px;
text-align: right;
width: 400px;
width: 800px;
}
#searchBox input {
#searchBox input#search {
width: 300px;
font-size: 16px;
background: #f9f8ed;
@@ -181,6 +190,7 @@ body {
padding: 20px;
overflow: auto;
float: left;
position: relative;
}
#books {
@@ -202,30 +212,25 @@ body {
}
.bookTable th[data-sort-by]::after {
content: "\f0dc";
font-family: FontAwesome;
font-size: x-small;
content: "\2195";
position: relative;
left: 4px;
bottom: 2px;
}
.bookTable th.asc::after {
content: "\f0de";
font-family: FontAwesome;
font-size: x-small;
content: "\2191";
font-size: small;
position: relative;
left: 4px;
bottom: 2px;
bottom: 1px;
}
.bookTable th.desc::after {
content: "\f0dd";
font-family: FontAwesome;
font-size: x-small;
content: "\2193";
font-size: small;
position: relative;
left: 4px;
bottom: 2px;
bottom: 1px;
}
.bookTable td,
@@ -243,10 +248,6 @@ body {
cursor: pointer;
}
.bookTable .onLoan {
color: #bbb;
}
.bookTable .tRow .title {
font-style: italic;
max-width: 600px;
@@ -256,7 +257,7 @@ body {
font-size: x-large;
font-weight: bold;
font-style: italic;
padding: 10px 0;
padding: 0 0 5px 0;
}
#current h2 {
@@ -265,20 +266,23 @@ body {
}
#current img {
max-height: 400px;
max-width: 100%;
display: block;
margin: 0 auto;
opacity: 0.5;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: auto;
}
#current .bookDetails {
position: relative;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px;
margin: 0;
width: 75%;
border-radius: 5px;
}
#current .description p {
padding: 20px 0;
}
#current h1.onLoan {
color: #bbb;
}
#current h2.onLoan {
font-weight: bold;
}

82
go.mod
View File

@@ -1,9 +1,85 @@
module git.yetaga.in/alazyreader/library
go 1.16
go 1.24.1
require (
github.com/gdamore/tcell v1.4.0
github.com/go-sql-driver/mysql v1.6.0
git.yetaga.in/alazyreader/go-openlibrary v0.0.1
github.com/go-sql-driver/mysql v1.9.3
github.com/irlndts/go-discogs v0.3.6
github.com/kelseyhightower/envconfig v1.4.0
golang.org/x/sync v0.17.0
tailscale.com v1.84.3
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/aws/aws-sdk-go-v2 v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/csrf v1.7.3 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect
)

272
go.sum
View File

@@ -1,16 +1,260 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
git.yetaga.in/alazyreader/go-openlibrary v0.0.1 h1:5juCi8d7YyNxXFvHytQNBww5E6GmPetM7nz3kVUqNQY=
git.yetaga.in/alazyreader/go-openlibrary v0.0.1/go.mod h1:o6zBFJTovdFcpA+As1bRFvk5PDhAs2Lf8iVzUt7dKw8=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/irlndts/go-discogs v0.3.6 h1:3oIJEkLGQ1ffJcoo6wvtawPI4/SyHoRpnu25Y51U4wg=
github.com/irlndts/go-discogs v0.3.6/go.mod h1:UVQ05FdCzH4P/usnSxQDh77QYE37HvmPnSCgogioljo=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U=
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.84.1 h1:xtuiYeAIUR+dRztPzzqUsjj+Fv/06vz28zoFaP1k/Os=
tailscale.com v1.84.1/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=
tailscale.com v1.84.2 h1:v6aM4RWUgYiV52LRAx6ET+dlGnvO/5lnqPXb7/pMnR0=
tailscale.com v1.84.2/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=
tailscale.com v1.84.3 h1:Ur9LMedSgicwbqpy5xn7t49G8490/s6rqAJOk5Q5AYE=
tailscale.com v1.84.3/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=

View File

@@ -5,17 +5,17 @@ import (
"io"
"strings"
"git.yetaga.in/alazyreader/library/book"
"git.yetaga.in/alazyreader/library/media"
)
func CSVToBooks(r io.Reader) ([]book.Book, error) {
func CSVToBooks(r io.Reader) ([]media.Book, error) {
reader := csv.NewReader(r)
header, err := reader.Read()
if err != nil {
return nil, err
}
hmap := parseHeader(header)
books := []book.Book{}
books := []media.Book{}
for {
row, err := reader.Read()
@@ -25,7 +25,7 @@ func CSVToBooks(r io.Reader) ([]book.Book, error) {
if err != nil {
return books, err
}
b := book.Book{
b := media.Book{
Title: row[hmap["title"]],
Authors: parseAuthors(row[hmap["author"]]),
SortAuthor: row[hmap["authorlast"]],
@@ -40,7 +40,6 @@ func CSVToBooks(r io.Reader) ([]book.Book, error) {
Signed: row[hmap["signed"]] == "yes", // convert from known string to bool
Description: row[hmap["description"]],
Notes: row[hmap["notes"]],
OnLoan: row[hmap["onloan"]],
CoverURL: row[hmap["coverurl"]],
}
books = append(books, b)

View File

@@ -1,4 +1,4 @@
package book
package media
type Book struct {
ID int `json:"-"`
@@ -16,6 +16,21 @@ type Book struct {
Signed bool `json:"signed"`
Description string `json:"description"`
Notes string `json:"notes"`
OnLoan string `json:"onLoan"`
CoverURL string `json:"coverURL"`
Childrens bool `json:"childrens"`
}
type Record struct {
ID int `json:"-"`
AlbumName string `json:"name"`
Artists []string `json:"artists"`
SortArtist string `json:"sortArtist"`
Identifier string `json:"identifier"`
Format string `json:"format"`
Genre string `json:"genre"`
Label string `json:"label"`
Year string `json:"year"`
Description string `json:"description"`
CoverURL string `json:"coverURL"`
DiscogsURL string `json:"discogsURL"`
}

13
query/amazon.go Normal file
View File

@@ -0,0 +1,13 @@
package query
import (
"fmt"
"git.yetaga.in/alazyreader/library/media"
)
type Amazon struct{}
func (o *Amazon) GetByISBN(isbn string) (*media.Book, error) {
return nil, fmt.Errorf("unimplemented")
}

28
query/funcs.go Normal file
View File

@@ -0,0 +1,28 @@
package query
import (
"fmt"
"strings"
)
func tryGetFirst(s []string) string {
if len(s) == 0 {
return ""
}
return s[0]
}
func buildTitle(title, subtitle string) string {
if subtitle != "" {
return fmt.Sprintf("%s: %s", title, subtitle)
}
return title
}
func getLastName(author string) string {
names := strings.Split(author, " ")
if len(names) < 2 {
return author
}
return names[len(names)-1]
}

158
query/googlebooks.go Normal file
View File

@@ -0,0 +1,158 @@
package query
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"git.yetaga.in/alazyreader/library/media"
)
type GoogleBooks struct{}
type googleBookResult struct {
Kind string `json:"kind"`
TotalItems int `json:"totalItems"`
Items []item `json:"items"`
}
type industryIdentifier struct {
Type string `json:"type"`
Identifier string `json:"identifier"`
}
type readingMode struct {
Text bool `json:"text"`
Image bool `json:"image"`
}
type panelizationSummary struct {
ContainsEpubBubbles bool `json:"containsEpubBubbles"`
ContainsImageBubbles bool `json:"containsImageBubbles"`
}
type imageLink struct {
SmallThumbnail string `json:"smallThumbnail"`
Thumbnail string `json:"thumbnail"`
}
type volumeInfo struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Authors []string `json:"authors"`
Publisher string `json:"publisher"`
PublishedDate string `json:"publishedDate"`
Description string `json:"description"`
IndustryIdentifiers []industryIdentifier `json:"industryIdentifiers"`
ReadingModes readingMode `json:"readingModes"`
PageCount int `json:"pageCount"`
PrintType string `json:"printType"`
Categories []string `json:"categories"`
AverageRating float64 `json:"averageRating"`
RatingsCount int `json:"ratingsCount"`
MaturityRating string `json:"maturityRating"`
AllowAnonLogging bool `json:"allowAnonLogging"`
ContentVersion string `json:"contentVersion"`
PanelizationSummary panelizationSummary `json:"panelizationSummary"`
ImageLinks imageLink `json:"imageLinks"`
Language string `json:"language"`
PreviewLink string `json:"previewLink"`
InfoLink string `json:"infoLink"`
CanonicalVolumeLink string `json:"canonicalVolumeLink"`
}
type saleInfo struct {
Country string `json:"country"`
Saleability string `json:"saleability"`
IsEbook bool `json:"isEbook"`
}
type epub struct {
IsAvailable bool `json:"isAvailable"`
}
type pdf struct {
IsAvailable bool `json:"isAvailable"`
}
type accessInfo struct {
Country string `json:"country"`
Viewability string `json:"viewability"`
Embeddable bool `json:"embeddable"`
PublicDomain bool `json:"publicDomain"`
TextToSpeechPermission string `json:"textToSpeechPermission"`
Epub epub `json:"epub"`
Pdf pdf `json:"pdf"`
WebReaderLink string `json:"webReaderLink"`
AccessViewStatus string `json:"accessViewStatus"`
QuoteSharingAllowed bool `json:"quoteSharingAllowed"`
}
type searchInfo struct {
TextSnippet string `json:"textSnippet"`
}
type item struct {
Kind string `json:"kind"`
ID string `json:"id"`
Etag string `json:"etag"`
SelfLink string `json:"selfLink"`
VolumeInfo volumeInfo `json:"volumeInfo"`
SaleInfo saleInfo `json:"saleInfo"`
AccessInfo accessInfo `json:"accessInfo"`
SearchInfo searchInfo `json:"searchInfo"`
}
func (g *GoogleBooks) GetByISBN(isbn string) (*media.Book, error) {
client := &http.Client{}
resp, err := client.Get(fmt.Sprintf("https://www.googleapis.com/books/v1/volumes?q=isbn:%s", isbn))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-200 status code: %d", resp.StatusCode)
}
var result googleBookResult
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(b, &result); err != nil {
return nil, err
}
if len(result.Items) == 0 {
return nil, fmt.Errorf("no book found")
}
return googleToBook(result.Items[0]), nil
}
func googleToBook(i item) *media.Book {
return &media.Book{
Title: buildTitle(i.VolumeInfo.Title, i.VolumeInfo.Subtitle),
Authors: i.VolumeInfo.Authors,
SortAuthor: strings.ToLower(getLastName(tryGetFirst(i.VolumeInfo.Authors))),
ISBN10: getIdentifierType(i.VolumeInfo.IndustryIdentifiers, "ISBN_10"),
ISBN13: getIdentifierType(i.VolumeInfo.IndustryIdentifiers, "ISBN_13"),
Publisher: i.VolumeInfo.Publisher,
Year: strings.Split(i.VolumeInfo.PublishedDate, "-")[0],
Description: i.VolumeInfo.Description,
Genre: tryGetFirst(i.VolumeInfo.Categories),
}
}
func getIdentifierType(iis []industryIdentifier, typename string) string {
for _, ident := range iis {
if ident.Type == typename {
return ident.Identifier
}
}
return ""
}

11
query/null.go Normal file
View File

@@ -0,0 +1,11 @@
package query
import (
"git.yetaga.in/alazyreader/library/media"
)
type Null struct{}
func (o *Null) GetByISBN(isbn string) (*media.Book, error) {
return nil, nil
}

46
query/openlibrary.go Normal file
View File

@@ -0,0 +1,46 @@
package query
import (
"strings"
"git.yetaga.in/alazyreader/go-openlibrary/client"
"git.yetaga.in/alazyreader/library/media"
)
type OpenLibrary struct {
client client.Client
}
func (o *OpenLibrary) GetByISBN(isbn string) (*media.Book, error) {
details, err := o.client.GetByISBN(isbn)
if err != nil {
return nil, err
}
return openLibraryToBook(details), nil
}
func openLibraryToBook(details *client.BookDetails) *media.Book {
return &media.Book{
Title: details.Title,
Authors: getAuthors(details.Authors),
SortAuthor: strings.ToLower(getLastName(tryGetFirst(getAuthors(details.Authors)))),
Publisher: getPublisher(details.Publishers),
ISBN10: tryGetFirst(details.Identifiers.ISBN10),
ISBN13: tryGetFirst(details.Identifiers.ISBN13),
}
}
func getPublisher(publishers []client.Publishers) string {
if len(publishers) == 0 {
return ""
}
return publishers[0].Name
}
func getAuthors(authors []client.Authors) []string {
ret := make([]string, len(authors))
for _, author := range authors {
ret = append(ret, author.Name)
}
return ret
}

5
readme.md Normal file
View File

@@ -0,0 +1,5 @@
# library
[![build status](https://ci.yetaga.in/api/badges/alazyreader/library/status.svg)](https://ci.yetaga.in/alazyreader/library)
A slowly growing list of most of the media I own.

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -1,116 +0,0 @@
package ui
import (
"fmt"
"github.com/gdamore/tcell"
)
type coord struct {
x, y int
}
type MockScreen struct {
x, y, h, w int
content map[coord]rune
}
func (m *MockScreen) Init() error {
m.content = map[coord]rune{}
return nil
}
func (m *MockScreen) Fini() {}
func (m *MockScreen) Clear() {
m.content = map[coord]rune{}
}
func (m *MockScreen) Fill(rune, tcell.Style) {}
func (m *MockScreen) SetCell(x int, y int, style tcell.Style, ch ...rune) {}
func (m *MockScreen) GetContent(x, y int) (mainc rune, combc []rune, style tcell.Style, width int) {
return m.content[coord{x, y}], nil, tcell.StyleDefault, 1
}
func (m *MockScreen) SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) {
m.content[coord{x, y}] = mainc
}
func (m *MockScreen) SetStyle(style tcell.Style) {}
func (m *MockScreen) ShowCursor(x int, y int) {}
func (m *MockScreen) HideCursor() {}
func (m *MockScreen) Size() (int, int) {
return m.h, m.w
}
func (m *MockScreen) PollEvent() tcell.Event {
return tcell.NewEventError(fmt.Errorf("mock error"))
}
func (m *MockScreen) PostEvent(ev tcell.Event) error {
return nil
}
func (m *MockScreen) PostEventWait(ev tcell.Event) {}
func (m *MockScreen) EnableMouse() {}
func (m *MockScreen) DisableMouse() {}
func (m *MockScreen) HasMouse() bool {
return false
}
func (m *MockScreen) Colors() int {
return 0
}
func (m *MockScreen) Show() {}
func (m *MockScreen) Sync() {}
func (m *MockScreen) CharacterSet() string {
return "UTF-8"
}
func (m *MockScreen) RegisterRuneFallback(r rune, subst string) {}
func (m *MockScreen) UnregisterRuneFallback(r rune) {}
func (m *MockScreen) CanDisplay(r rune, checkFallbacks bool) bool {
return true
}
func (m *MockScreen) Resize(x, y, h, w int) {
m.x, m.y, m.h, m.w = x, y, h, w
}
func (m *MockScreen) HasKey(tcell.Key) bool {
return true
}
func (m *MockScreen) Beep() error {
return nil
}
func (m *MockScreen) DumpContents() string {
var res string
for i := m.y; i < m.h; i++ {
str := []rune{}
for j := m.x; j < m.w; j++ {
r, ok := m.content[coord{x: j, y: i}]
if ok {
str = append(str, r)
} else {
str = append(str, ' ')
}
}
res = res + string(str) + "\n"
}
return res
}

617
ui/ui.go
View File

@@ -1,617 +0,0 @@
package ui
import (
"strconv"
"strings"
"git.yetaga.in/alazyreader/library/book"
"github.com/gdamore/tcell"
)
type Drawable interface {
Draw(tcell.Screen)
SetSize(x, y, h, w int)
SetStyle(tcell.Style)
SetVisible(bool)
}
type Offsets struct {
Top int
Bottom int
Left int
Right int
Percent int
}
type Contents []struct {
Offsets Offsets
Container Drawable
}
const (
LayoutUnmanaged = iota
LayoutHorizontalEven
LayoutVerticalEven
LayoutHorizontalPercent
LayoutVerticalPercent
)
var (
StyleActive = tcell.Style(0).Foreground(tcell.ColorWhite).Background(tcell.ColorBlack)
StyleInactive = tcell.Style(0).Foreground(tcell.ColorGray).Background(tcell.ColorBlack)
)
// A Container has no visible UI of its own, but arranges sub-components on the screen.
// layoutMethod manages how subcomponents are organized. If `LayoutUnmanaged`, it just uses the offsets
// in contents to paint on the screen. Otherwise, `LayoutHorizontalEven` and `LayoutVerticalEven` will
// have it compute even distributions of space for all components either horizontally or vertically,
// filling the container.
type Container struct {
x, y int
h, w int
layoutMethod int
contents Contents
visible bool
}
func NewContainer(contents Contents, layoutMethod int) *Container {
return &Container{
layoutMethod: layoutMethod,
contents: contents,
visible: true,
}
}
func (c *Container) Draw(s tcell.Screen) {
if !c.visible {
return
}
for i := range c.contents {
c.contents[i].Container.Draw(s)
}
}
func (c *Container) SetSize(x, y, h, w int) {
c.x, c.y, c.h, c.w = x, y, h, w
carry := 0
if c.layoutMethod == LayoutVerticalEven {
num := len(c.contents)
extra := c.h % num
for r := range c.contents {
w := c.w
h := c.h / num
x := c.x
y := c.y + (h * r) + carry
if extra > 0 { // distribute "extra" space to containers as we have some left
h++
extra--
carry++
}
c.contents[r].Container.SetSize(x, y, h, w)
}
} else if c.layoutMethod == LayoutHorizontalEven {
num := len(c.contents)
extra := c.w % num
for r := range c.contents {
w := c.w / num
h := c.h
x := c.x + (w * r) + carry
y := c.y
if extra > 0 { // distribute "extra" space to containers as we have some left
w++
extra--
carry++
}
c.contents[r].Container.SetSize(x, y, h, w)
}
} else if c.layoutMethod == LayoutHorizontalPercent {
// first, work out overall distribution
total := 0
for r := range c.contents {
// `0` or negatives are set as minimum
if c.contents[r].Offsets.Percent < 1 {
total += 1
} else {
total += c.contents[r].Offsets.Percent
}
}
carry := 0
// push around containers
for r := range c.contents {
ratio := (float64(c.contents[r].Offsets.Percent) / float64(total))
w := int(float64(c.w) * ratio)
h := c.h
x := c.x + carry
y := c.y
carry += w
// and add any remaining space to the last container
if r == len(c.contents)-1 {
w += (c.w - carry)
}
c.contents[r].Container.SetSize(x, y, h, w)
}
} else if c.layoutMethod == LayoutVerticalPercent {
// first, work out overall distribution
total := 0
for r := range c.contents {
// `0` or negatives are set as minimum
if c.contents[r].Offsets.Percent < 1 {
total += 1
} else {
total += c.contents[r].Offsets.Percent
}
}
carry := 0
// push around containers
for r := range c.contents {
ratio := (float64(c.contents[r].Offsets.Percent) / float64(total))
w := c.w
h := int(float64(c.h) * ratio)
x := c.x
y := c.y + carry
carry += h
// and add any remaining space to the last container
if r == len(c.contents)-1 {
h += (c.h - carry)
}
c.contents[r].Container.SetSize(x, y, h, w)
}
} else {
for r := range c.contents {
x := c.x + c.contents[r].Offsets.Left
y := c.y + c.contents[r].Offsets.Top
h := c.h - c.contents[r].Offsets.Bottom
w := c.w - c.contents[r].Offsets.Right
c.contents[r].Container.SetSize(x, y, h, w)
}
}
}
func (c *Container) SetStyle(s tcell.Style) {
// containers have no visible elements to style
}
func (c *Container) SetVisible(b bool) {
c.visible = b
}
func (c *Container) Contents() Contents {
return c.contents
}
func (c *Container) SetContents(con Contents) {
c.contents = con
}
// A Box draws a ASCII box around its contents, with an optional title and footer.
type Box struct {
x, y int
h, w int
title Drawable
menuItems Drawable
contents Contents
style tcell.Style
cascade bool
visible bool
transparent bool
}
func NewBox(title string, menuItems []string, contents Contents, initialStyle tcell.Style, cascade bool) *Box {
return &Box{
title: NewPaddedText(title),
menuItems: NewPaddedText(strings.Join(menuItems, " ")),
contents: contents,
style: initialStyle,
cascade: cascade,
visible: true,
transparent: false,
}
}
func (b *Box) SetSize(x, y, h, w int) {
b.x, b.y, b.h, b.w = x, y, h, w
b.title.SetSize(b.x+2, b.y, 0, 0)
b.menuItems.SetSize(b.x+2, b.y+b.h-1, 0, 0)
for c := range b.contents {
x := b.x + b.contents[c].Offsets.Left
y := b.y + b.contents[c].Offsets.Top
h := b.h - b.contents[c].Offsets.Bottom
w := b.w - b.contents[c].Offsets.Right
b.contents[c].Container.SetSize(x, y, h, w)
}
}
func (b *Box) Draw(s tcell.Screen) {
if !b.visible {
return
}
// blank out inner area
if !b.transparent {
for m := b.x + 1; m < b.x+b.w-1; m++ {
for n := b.y + 1; n < b.y+b.h-1; n++ {
s.SetContent(m, n, ' ', nil, b.style)
}
}
}
// draw outside bars
for m := b.x + 1; m < b.x+b.w-1; m++ {
s.SetContent(m, b.y, tcell.RuneHLine, nil, b.style)
s.SetContent(m, b.y+b.h-1, tcell.RuneHLine, nil, b.style)
}
for m := b.y + 1; m < b.y+b.h-1; m++ {
s.SetContent(b.x, m, tcell.RuneVLine, nil, b.style)
s.SetContent(b.x+b.w-1, m, tcell.RuneVLine, nil, b.style)
}
s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, b.style)
s.SetContent(b.x+b.w-1, b.y, tcell.RuneURCorner, nil, b.style)
s.SetContent(b.x, b.y+b.h-1, tcell.RuneLLCorner, nil, b.style)
s.SetContent(b.x+b.w-1, b.y+b.h-1, tcell.RuneLRCorner, nil, b.style)
if b.title != nil {
b.title.Draw(s)
}
if b.menuItems != nil {
b.menuItems.Draw(s)
}
for c := range b.contents {
b.contents[c].Container.Draw(s)
}
}
func (b *Box) SetStyle(s tcell.Style) {
b.style = s
b.title.SetStyle(s)
b.menuItems.SetStyle(s)
if b.cascade {
for c := range b.contents {
b.contents[c].Container.SetStyle(s)
}
}
}
func (b *Box) SetVisible(v bool) {
b.visible = v
}
func (b *Box) SetTransparent(v bool) {
b.transparent = v
}
func (b *Box) Contents() Contents {
return b.contents
}
func (b *Box) SetContents(c Contents) {
b.contents = c
}
// A List is a scrollable, pageable list with a selector token.
type List struct {
x, y int
h, w int
selected int
listItems []ListKeyValue
style tcell.Style
visible bool
}
type ListKeyValue struct {
Key int
Value string
}
func NewList(listItems []ListKeyValue, initialSelected int) *List {
return &List{
listItems: listItems,
selected: initialSelected,
visible: true,
}
}
func (l *List) SetSize(x, y, h, w int) {
l.x, l.y, l.h, l.w = x, y, h, w
}
func (l *List) Draw(s tcell.Screen) {
if !l.visible {
return
}
for i := range l.listItems {
for j, r := range l.listItems[i].Value {
s.SetContent(l.x+j, l.y+i, r, nil, l.style)
}
if i == l.selected {
s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, l.style)
}
}
}
func (l *List) SetVisible(b bool) {
l.visible = b
}
func (l *List) SetStyle(s tcell.Style) {
l.style = s
}
func (l *List) Selected() int {
return l.selected
}
func (l *List) SelectedID() int {
if l.listItems == nil || len(l.listItems) == 0 {
return 0
}
return l.listItems[l.selected].Key
}
func (l *List) SetSelected(i int) {
l.selected = i
}
func (l *List) ListMembers() []ListKeyValue {
return l.listItems
}
func (l *List) SetMembers(lkv []ListKeyValue) {
l.listItems = lkv
}
// BookDetails displays an editable list of book details
type BookDetails struct {
x, y int
h, w int
book *book.Book
style tcell.Style
visible bool
}
func NewBookDetails(b *book.Book) *BookDetails {
return &BookDetails{
book: b,
visible: true,
}
}
func (l *BookDetails) SetBook(b *book.Book) {
l.book = b
}
func (l *BookDetails) SetSize(x, y, h, w int) {
l.x, l.y, l.h, l.w = x, y, h, w
}
func (l *BookDetails) Draw(s tcell.Screen) {
if l.book == nil {
return
}
if !l.visible {
return
}
items := []struct {
label string
value string
}{
{"Title", l.book.Title},
{"Authors", strings.Join(l.book.Authors, ", ")},
{"Sort Author", l.book.SortAuthor},
{"ISBN-10", l.book.ISBN10},
{"ISBN-13", l.book.ISBN13},
{"Format", l.book.Format},
{"Genre", l.book.Genre},
{"Publisher", l.book.Publisher},
{"Series", l.book.Series},
{"Volume", l.book.Volume},
{"Year", l.book.Year},
{"Signed", strconv.FormatBool(l.book.Signed)},
{"On Loan", l.book.OnLoan},
{"Cover URL", l.book.CoverURL},
{"Notes", l.book.Notes},
{"Description", l.book.Description},
}
for i := range items {
if i < l.h-2 {
kv := NewKeyValue(items[i].label, ": ", items[i].value)
kv.SetSize(l.x, l.y+i, 0, 0)
kv.SetStyle(l.style)
kv.Draw(s)
}
}
}
func (l *BookDetails) SetVisible(b bool) {
l.visible = b
}
func (l *BookDetails) SetStyle(s tcell.Style) {
l.style = s
}
// PaddedText outputs strings with a space on both sides.
// Useful for generating headings, footers, etc. Used by Box.
type PaddedText struct {
x, y int
h, w int
text string
style tcell.Style
visible bool
}
func NewPaddedText(text string) *PaddedText {
return &PaddedText{text: text, visible: true}
}
func (p *PaddedText) SetSize(x, y, _, _ int) {
p.x, p.y, p.h, p.w = x, y, 1, len(p.text)+2
}
func (p *PaddedText) SetStyle(s tcell.Style) {
p.style = s
}
func (p *PaddedText) Draw(s tcell.Screen) {
if p.text == "" {
return
}
if !p.visible {
return
}
t := p.x
s.SetContent(t, p.y, ' ', nil, p.style)
t++
for _, r := range p.text {
s.SetContent(t, p.y, r, nil, p.style)
t++
}
s.SetContent(t, p.y, ' ', nil, p.style)
}
func (p *PaddedText) SetVisible(b bool) {
p.visible = b
}
type KeyValue struct {
x, y int
h, w int
key string
value string
separator string
style tcell.Style
visible bool
}
func NewKeyValue(key, separator, value string) *KeyValue {
return &KeyValue{
key: key,
separator: separator,
value: value,
visible: true,
}
}
func (p *KeyValue) SetSize(x, y, _, _ int) {
p.x, p.y, p.h, p.w = x, y, 1, len(p.key)+len(p.separator)+len(p.value)
}
func (p *KeyValue) SetStyle(s tcell.Style) {
p.style = s
}
func (p *KeyValue) Draw(s tcell.Screen) {
if !p.visible {
return
}
for j, r := range p.key {
s.SetContent(p.x+j, p.y, r, nil, p.style)
}
for j, r := range p.separator {
s.SetContent(p.x+len(p.key)+j, p.y, r, nil, p.style)
}
for j, r := range p.value {
s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, p.style)
}
}
func (p *KeyValue) SetVisible(b bool) {
p.visible = b
}
func (p *KeyValue) GetValue() string {
return p.value
}
type EditableTextLine struct {
x, y int
h, w int
text string
style tcell.Style
visible bool
cursorPos int
showCursor bool
}
func NewEditableTextLine(initialText string) *EditableTextLine {
return &EditableTextLine{
text: initialText,
visible: true,
showCursor: true,
}
}
func (p *EditableTextLine) SetSize(x, y, _, _ int) {
p.x, p.y, p.h, p.w = x, y, 1, len(p.text)
}
func (p *EditableTextLine) SetStyle(s tcell.Style) {
p.style = s
}
func (p *EditableTextLine) Draw(s tcell.Screen) {
if !p.visible {
return
}
for j, r := range p.text {
s.SetContent(p.x+j, p.y, r, nil, p.style)
}
s.ShowCursor(p.x+p.cursorPos, p.y)
}
func (p *EditableTextLine) SetVisible(b bool) {
p.visible = b
}
func (p *EditableTextLine) SetCursorVisible(b bool) {
p.showCursor = b
}
func (p *EditableTextLine) SetText(t string) {
p.text = t
if len(p.text) == 0 {
p.ResetCursor(true)
return
}
p.ResetCursor(false)
}
func (p *EditableTextLine) Text() string {
return p.text
}
func (p *EditableTextLine) ResetCursor(beginning bool) {
if beginning {
p.cursorPos = 0
} else {
p.cursorPos = len(p.text)
}
}
func (p *EditableTextLine) InsertAtCursor(r rune) {
if len(p.text) == 0 {
p.text = string(r)
p.cursorPos = 1
return
}
p.text = p.text[0:p.cursorPos] + string(r) + p.text[p.cursorPos:len(p.text)]
p.cursorPos = p.cursorPos + 1
}
func (p *EditableTextLine) MoveCursor(i int) {
if p.cursorPos+i < 0 {
p.cursorPos = 0
return
}
if p.cursorPos+i > len(p.text) {
p.cursorPos = len(p.text)
return
}
p.cursorPos = p.cursorPos + i
}
func (p *EditableTextLine) DeleteAtCursor() {
if len(p.text) == 0 {
p.cursorPos = 0
return
}
p.text = p.text[0:p.cursorPos-1] + p.text[p.cursorPos:len(p.text)]
p.cursorPos = p.cursorPos - 1
}

View File

@@ -1,239 +0,0 @@
package ui
import (
"fmt"
"testing"
)
func TestContainerOneBox(t *testing.T) {
expect := `┌─ box one ────────┐
│ │
│ │
│ │
└──────────────────┘
`
m := &MockScreen{}
one := NewBox("box one", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}},
LayoutHorizontalEven,
)
m.Init()
m.Resize(0, 0, 5, 20)
container.SetSize(0, 0, 5, 20)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestContainerTwoBoxesHStack(t *testing.T) {
expect := `┌─ one ──┐┌─ two ──┐
│ ││ │
│ ││ │
│ ││ │
└────────┘└────────┘
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}},
LayoutHorizontalEven,
)
m.Init()
m.Resize(0, 0, 5, 20)
container.SetSize(0, 0, 5, 20)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestContainerThreeBoxesUnevenHStack(t *testing.T) {
expect := `┌─ one ──┐┌─ two ──┐┌─ three
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
└────────┘└────────┘└───────┘
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
three := NewBox("three", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}, {Container: three}},
LayoutHorizontalEven,
)
m.Init()
m.Resize(0, 0, 5, 29)
container.SetSize(0, 0, 5, 29)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestContainerTwoBoxesHPercentStack(t *testing.T) {
expect := `┌─ one ──────┐┌─ two ┐
│ ││ │
│ ││ │
│ ││ │
└────────────┘└──────┘
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{
{Container: one, Offsets: Offsets{Percent: 2}},
{Container: two, Offsets: Offsets{Percent: 1}}},
LayoutHorizontalPercent,
)
m.Init()
m.Resize(0, 0, 5, 22)
container.SetSize(0, 0, 5, 22)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestContainerTwoBoxesVStack(t *testing.T) {
expect := `┌─ one ──┐
│ │
│ │
│ │
└────────┘
┌─ two ──┐
│ │
│ │
│ │
└────────┘
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}},
LayoutVerticalEven,
)
m.Init()
m.Resize(0, 0, 10, 10)
container.SetSize(0, 0, 10, 10)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestContainerTwoBoxesPercentageVStack(t *testing.T) {
expect := `┌─ one ──┐
│ │
│ │
│ │
│ │
└────────┘
┌─ two ──┐
│ │
│ │
└────────┘
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{
{Container: one, Offsets: Offsets{Percent: 2}},
{Container: two, Offsets: Offsets{Percent: 1}}},
LayoutVerticalPercent,
)
m.Init()
m.Resize(0, 0, 10, 10)
container.SetSize(0, 0, 10, 10)
container.Draw(m)
result := m.DumpContents()
if result != expect {
fmt.Printf("expected:\n%+v", expect)
fmt.Printf("actual:\n%+v", result)
t.Fail()
}
}
func TestNewEditableTextLine(t *testing.T) {
e := NewEditableTextLine("")
e.InsertAtCursor('a')
e.InsertAtCursor('b')
e.InsertAtCursor('c')
if e.text != "abc" {
fmt.Printf("expected: 'abc', actual: '%+v'", e.text)
t.Fail()
}
e.MoveCursor(-1)
e.InsertAtCursor('d')
if e.text != "abdc" {
fmt.Printf("expected: 'abdc', actual: '%+v'", e.text)
t.Fail()
}
e.MoveCursor(-20)
e.InsertAtCursor('e')
if e.text != "eabdc" {
fmt.Printf("expected: 'eabdc', actual: '%+v'", e.text)
t.Fail()
}
e.MoveCursor(20)
e.InsertAtCursor('f')
if e.text != "eabdcf" {
fmt.Printf("expected: 'eabdcf', actual: '%+v'", e.text)
t.Fail()
}
e.MoveCursor(1)
e.InsertAtCursor('g')
if e.text != "eabdcfg" {
fmt.Printf("expected: 'eabdcfg', actual: '%+v'", e.text)
t.Fail()
}
e.DeleteAtCursor()
e.DeleteAtCursor()
e.MoveCursor(-1)
e.DeleteAtCursor()
if e.text != "eabc" {
fmt.Printf("expected: 'eabc', actual: '%+v'", e.text)
t.Fail()
}
e.ResetCursor(false)
e.InsertAtCursor('h')
e.ResetCursor(true)
e.InsertAtCursor('g')
if e.text != "geabch" {
fmt.Printf("expected: 'geabch', actual: '%+v'", e.text)
t.Fail()
}
e.SetText("the rain in spain")
e.InsertAtCursor('s')
if e.text != "the rain in spains" {
fmt.Printf("expected: 'the rain in spains', actual: '%+v'", e.text)
t.Fail()
}
e.SetText("")
e.InsertAtCursor('s')
if e.text != "s" {
fmt.Printf("expected: 's', actual: '%+v'", e.text)
t.Fail()
}
}