[WIP] music platform user data scraper
teal-fm atproto

that should be all the layouts

+2 -2
.air.toml
··· 14 14 follow_symlink = false 15 15 full_bin = "" 16 16 include_dir = [] 17 - include_ext = ["go", "tpl", "tmpl", "html", "gohtml"] 17 + include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css", "js"] 18 18 include_file = [] 19 19 kill_delay = "0s" 20 20 log = "build-errors.log" ··· 48 48 proxy_port = 0 49 49 50 50 [screen] 51 - clear_on_rebuild = false 51 + clear_on_rebuild = true 52 52 keep_scroll = true
+17 -30
cmd/handlers.go
··· 50 50 } 51 51 } 52 52 53 - func handleLinkLastfmForm(database *db.DB) http.HandlerFunc { 53 + func handleLinkLastfmForm(database *db.DB, pg *pages.Pages) http.HandlerFunc { 54 54 return func(w http.ResponseWriter, r *http.Request) { 55 - userID, _ := session.GetUserID(r.Context()) 55 + userID, authenticated := session.GetUserID(r.Context()) 56 56 if r.Method == http.MethodPost { 57 57 if err := r.ParseForm(); err != nil { 58 58 http.Error(w, "Failed to parse form", http.StatusBadRequest) ··· 87 87 } 88 88 89 89 w.Header().Set("Content-Type", "text/html") 90 - fmt.Fprintf(w, ` 91 - <html> 92 - <head><title>Link Last.fm Account</title> 93 - <style> 94 - body { font-family: Arial, sans-serif; max-width: 600px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; } 95 - label, input { display: block; margin-bottom: 10px; } 96 - input[type='text'] { width: 95%%; padding: 8px; } /* Corrected width */ 97 - input[type='submit'] { padding: 10px 15px; background-color: #d51007; color: white; border: none; border-radius: 4px; cursor: pointer; } 98 - .nav { margin-bottom: 20px; } 99 - .nav a { margin-right: 10px; text-decoration: none; color: #1DB954; font-weight: bold; } 100 - .error { color: red; margin-bottom: 10px; } 101 - </style> 102 - </head> 103 - <body> 104 - <div class="nav"> 105 - <a href="/">Home</a> 106 - <a href="/link-lastfm">Link Last.fm</a> 107 - <a href="/logout">Logout</a> 108 - </div> 109 - <h2>Link Your Last.fm Account</h2> 110 - <p>Enter your Last.fm username to start tracking your scrobbles.</p> 111 - <form method="post" action="/link-lastfm"> 112 - <label for="lastfm_username">Last.fm Username:</label> 113 - <input type="text" id="lastfm_username" name="lastfm_username" value="%s" required> 114 - <input type="submit" value="Save Username"> 115 - </form> 116 - </body> 117 - </html>`, currentUsername) 90 + 91 + pageParams := struct { 92 + NavBar pages.NavBar 93 + CurrentUsername string 94 + }{ 95 + NavBar: pages.NavBar{ 96 + IsLoggedIn: authenticated, 97 + LastFMUsername: currentUsername, 98 + }, 99 + CurrentUsername: currentUsername, 100 + } 101 + err = pg.Execute("lastFMForm", w, pageParams) 102 + if err != nil { 103 + log.Printf("Error executing template: %v", err) 104 + } 118 105 } 119 106 } 120 107
-3
cmd/main.go
··· 116 116 lastfmInterval = 30 * time.Second 117 117 } 118 118 119 - //if err := spotifyService.LoadAllUsers(); err != nil { 120 - // log.Printf("Warning: Failed to preload Spotify users: %v", err) 121 - //} 122 119 go spotifyService.StartListeningTracker(trackerInterval) 123 120 124 121 go lastfmService.StartListeningTracker(lastfmInterval)
+4 -8
cmd/routes.go
··· 10 10 11 11 func (app *application) routes() http.Handler { 12 12 mux := http.NewServeMux() 13 - // Redirect /static to /static/ so it doesn't fall through to "/" handler 13 + 14 + //Handles static file routes 14 15 mux.Handle("/static/{file_name}", app.pages.Static()) 15 - //mux.HandleFunc("/static/{file_name}", func(w http.ResponseWriter, r *http.Request) { 16 - // w.Header().Set("Content-Type", "text/html") 17 - // 18 - // w.Write([]byte("Static files are served from /static/")) 19 - //}) 20 16 21 17 mux.HandleFunc("/", session.WithPossibleAuth(home(app.database, app.pages), app.sessionManager)) 22 18 ··· 30 26 mux.HandleFunc("/current-track", session.WithAuth(app.spotifyService.HandleCurrentTrack, app.sessionManager)) 31 27 mux.HandleFunc("/history", session.WithAuth(app.spotifyService.HandleTrackHistory, app.sessionManager)) 32 28 mux.HandleFunc("/api-keys", session.WithAuth(app.apiKeyService.HandleAPIKeyManagement(app.database, app.pages), app.sessionManager)) 33 - mux.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(app.database), app.sessionManager)) // GET form 34 - mux.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(app.database), app.sessionManager)) // POST submit - Changed route slightly 29 + mux.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(app.database, app.pages), app.sessionManager)) // GET form 30 + mux.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(app.database), app.sessionManager)) // POST submit - Changed route slightly 35 31 mux.HandleFunc("/logout", app.sessionManager.HandleLogout) 36 32 mux.HandleFunc("/debug/", session.WithAuth(app.sessionManager.HandleDebug, app.sessionManager)) 37 33
+4 -9
pages/pages.go
··· 119 119 } 120 120 121 121 func (p *Pages) Static() http.Handler { 122 - //if p.dev { 123 - // return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 124 - //} 125 122 126 123 sub, err := fs.Sub(Files, "static") 127 124 if err != nil { 128 - //p.logger.Error("no static dir found? that's crazy", "err", err) 129 125 panic(err) 130 126 } 131 - return http.StripPrefix("/static/", http.FileServer(http.FS(sub))) 132 - // Custom handler to apply Cache-Control headers for font files 133 - //return http.FileServer(http.FS(sub)) 134 - //return http.FileServer(http.FS(sub)) 127 + 128 + return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 135 129 } 136 130 137 131 func Cache(h http.Handler) http.Handler { 138 132 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 139 133 path := strings.Split(r.URL.Path, "?")[0] 140 - 134 + 135 + //We may want to change these, just took what tangled has and allows browser side caching 141 136 if strings.HasSuffix(path, ".css") { 142 137 // on day for css files 143 138 w.Header().Set("Cache-Control", "public, max-age=86400")
-32
pages/static/style.css
··· 1 - body { 2 - font-family: Arial, sans-serif; 3 - max-width: 800px; 4 - margin: 0 auto; 5 - padding: 20px; 6 - line-height: 1.6; 7 - } 8 - h1 { 9 - color: #1DB954; /* Spotify green */ 10 - } 11 - .nav { 12 - display: flex; 13 - flex-wrap: wrap; /* Allow wrapping on smaller screens */ 14 - margin-bottom: 20px; 15 - } 16 - .nav a { 17 - margin-right: 15px; 18 - margin-bottom: 5px; /* Add spacing below links */ 19 - text-decoration: none; 20 - color: #1DB954; 21 - font-weight: bold; 22 - } 23 - .card { 24 - border: 1px solid #ddd; 25 - border-radius: 8px; 26 - padding: 20px; 27 - margin-bottom: 20px; 28 - } 29 - .service-status { 30 - font-style: italic; 31 - color: #555; 32 - }
+124
pages/static/styles.css
··· 1 + body { 2 + font-family: Arial, sans-serif; 3 + max-width: 800px; 4 + margin: 0 auto; 5 + padding: 20px; 6 + line-height: 1.6; 7 + } 8 + 9 + 10 + h1 { 11 + color: #1DB954; /* Spotify green */ 12 + } 13 + 14 + .nav { 15 + display: flex; 16 + flex-wrap: wrap; /* Allow wrapping on smaller screens */ 17 + margin-bottom: 20px; 18 + } 19 + 20 + .nav a { 21 + margin-right: 15px; 22 + margin-bottom: 5px; /* Add spacing below links */ 23 + text-decoration: none; 24 + color: #1DB954; 25 + font-weight: bold; 26 + } 27 + 28 + .card { 29 + border: 1px solid #ddd; 30 + border-radius: 8px; 31 + padding: 20px; 32 + margin-bottom: 20px; 33 + } 34 + 35 + .service-status { 36 + font-style: italic; 37 + color: #555; 38 + } 39 + 40 + 41 + label, input { 42 + display: block; 43 + margin-bottom: 10px; 44 + } 45 + 46 + input[type='text'] { 47 + width: 95%; 48 + padding: 8px; 49 + } 50 + 51 + /* Corrected width */ 52 + input[type='submit'] { 53 + padding: 10px 15px; 54 + color: white; 55 + border: none; 56 + border-radius: 4px; 57 + cursor: pointer; 58 + } 59 + 60 + .last-fm-input { 61 + background-color: #d51007; 62 + } 63 + 64 + .teal-input { 65 + background-color: #1DB954; 66 + } 67 + 68 + .error { 69 + color: red; 70 + margin-bottom: 10px; 71 + } 72 + 73 + .lastfm-form { 74 + max-width: 600px; 75 + margin: 20px auto; 76 + padding: 20px; 77 + border: 1px solid #ddd; 78 + border-radius: 8px; 79 + } 80 + 81 + .card { 82 + border: 1px solid #ddd; 83 + border-radius: 8px; 84 + padding: 20px; 85 + margin-bottom: 20px; 86 + } 87 + table { 88 + width: 100%; 89 + border-collapse: collapse; 90 + } 91 + table th, table td { 92 + padding: 8px; 93 + text-align: left; 94 + border-bottom: 1px solid #ddd; 95 + } 96 + .key-value { 97 + font-family: monospace; 98 + padding: 10px; 99 + background-color: #f5f5f5; 100 + border: 1px solid #ddd; 101 + border-radius: 4px; 102 + word-break: break-all; 103 + } 104 + .new-key-alert { 105 + background-color: #f8f9fa; 106 + border-left: 4px solid #1DB954; 107 + padding: 15px; 108 + margin-bottom: 20px; 109 + } 110 + .btn { 111 + padding: 8px 16px; 112 + background-color: #1DB954; 113 + color: white; 114 + border: none; 115 + border-radius: 4px; 116 + cursor: pointer; 117 + } 118 + .btn-danger { 119 + background-color: #dc3545; 120 + } 121 + 122 + .teal-header { 123 + color: #1DB954; 124 + }
+4 -4
pages/templates/apiKeys.gohtml
··· 7 7 <h1>API Key Management</h1> 8 8 9 9 <div class="card"> 10 - <h2>Create New API Key</h2> 10 + <h2 class="teal-header">Create New API Key</h2> 11 11 <p>API keys allow programmatic access to your Piper account data.</p> 12 12 <form method="POST" action="/api-keys"> 13 13 <div style="margin-bottom: 15px;"> ··· 20 20 21 21 {{if .NewKeyID}} <!-- Changed from .NewKey to .NewKeyID for clarity --> 22 22 <div class="new-key-alert"> 23 - <h3>Your new API key (ID: {{.NewKeyID}}) has been created</h3> 23 + <h3 class="teal-header">Your new API key (ID: {{.NewKeyID}}) has been created</h3> 24 24 <!-- The message below is misleading if only the ID is shown. 25 25 Consider changing this text or modifying the flow to show the actual key once for HTML. --> 26 26 <p><strong>Important:</strong> If this is an ID, ensure you have copied the actual key if it was displayed previously. For keys generated via the API, the key is returned in the API response.</p> ··· 28 28 {{end}} 29 29 30 30 <div class="card"> 31 - <h2>Your API Keys</h2> 31 + <h2 class="teal-header">Your API Keys</h2> 32 32 {{if .Keys}} 33 33 <table> 34 34 <thead> ··· 60 60 </div> 61 61 62 62 <div class="card"> 63 - <h2>API Usage</h2> 63 + <h2 class="teal-header">API Usage</h2> 64 64 <p>To use your API key, include it in the Authorization header of your HTTP requests:</p> 65 65 <pre>Authorization: Bearer YOUR_API_KEY</pre> 66 66 <p>Or include it as a query parameter (less secure for the key itself):</p>
+2 -17
pages/templates/home.gohtml
··· 2 2 {{ define "content" }} 3 3 4 4 <h1>Piper - Multi-User Spotify & Last.fm Tracker via ATProto</h1> 5 - <div class="nav"> 6 - <a href="/">Home</a> 5 + {{ template "components/navBar" .NavBar }} 7 6 8 - {{if .NavBar.IsLoggedIn}} 9 - <a href="/current-track">Spotify Current</a> 10 - <a href="/history">Spotify History</a> 11 - <a href="/link-lastfm">Link Last.fm</a> 12 - {{ if .NavBar.LastFMUsername }} 13 - <a href="/lastfm/recent">Last.fm Recent</a> 14 - {{ end }} 15 - <a href="/api-keys">API Keys</a> 16 - <a href="/login/spotify">Connect Spotify Account</a> 17 - <a href="/logout">Logout</a> 18 - {{ else }} 19 - <a href="/login/atproto">Login with ATProto</a> 20 - {{ end }} 21 - </div> 22 7 23 8 <div class="card"> 24 9 <h2>Welcome to Piper</h2> ··· 53 38 <form action="/login/atproto"> 54 39 <label for="handle">handle:</label> 55 40 <input type="text" id="handle" name="handle" > 56 - <input type="submit" value="submit"> 41 + <input class="teal-input" type="submit" value="submit"> 57 42 </form> 58 43 59 44
+11 -7
pages/templates/lastFMForm.gohtml
··· 1 1 {{ define "content" }} 2 - <h2>Link Your Last.fm Account</h2> 3 - <p>Enter your Last.fm username to start tracking your scrobbles.</p> 4 - <form method="post" action="/link-lastfm"> 5 - <label for="lastfm_username">Last.fm Username:</label> 6 - <input type="text" id="lastfm_username" name="lastfm_username" value="%s" required> 7 - <input type="submit" value="Save Username"> 8 - </form> 2 + {{ template "components/navBar" .NavBar }} 3 + 4 + <div class="lastfm-form"> 5 + <h2>Link Your Last.fm Account</h2> 6 + <p>Enter your Last.fm username to start tracking your scrobbles.</p> 7 + <form method="post" action="/link-lastfm"> 8 + <label for="lastfm_username">Last.fm Username:</label> 9 + <input type="text" id="lastfm_username" name="lastfm_username" value="{{.CurrentUsername}}" required> 10 + <input class="last-fm-input" type="submit" value="Save Username"> 11 + </form> 12 + </div> 9 13 10 14 {{ end }}
+1 -35
pages/templates/layouts/base.gohtml
··· 3 3 <html lang="en"> 4 4 <head> 5 5 <title>Piper - Spotify & Last.fm Tracker</title> 6 - <link rel="stylesheet" href="~/static/style.css"> 7 - <style> 8 - body { 9 - font-family: Arial, sans-serif; 10 - max-width: 800px; 11 - margin: 0 auto; 12 - padding: 20px; 13 - line-height: 1.6; 14 - } 15 - h1 { 16 - color: #1DB954; /* Spotify green */ 17 - } 18 - .nav { 19 - display: flex; 20 - flex-wrap: wrap; /* Allow wrapping on smaller screens */ 21 - margin-bottom: 20px; 22 - } 23 - .nav a { 24 - margin-right: 15px; 25 - margin-bottom: 5px; /* Add spacing below links */ 26 - text-decoration: none; 27 - color: #1DB954; 28 - font-weight: bold; 29 - } 30 - .card { 31 - border: 1px solid #ddd; 32 - border-radius: 8px; 33 - padding: 20px; 34 - margin-bottom: 20px; 35 - } 36 - .service-status { 37 - font-style: italic; 38 - color: #555; 39 - } 40 - </style> 6 + <link rel="stylesheet" href="/static/styles.css"> 41 7 </head> 42 8 <body> 43 9 {{ block "content" . }}{{ end }}