Skip to content

Commit 8bf093a

Browse files
committed
refactor: split god files and extract constants for clean code
Split 4 large files into focused modules following single responsibility: - main.go (1764→299 lines): extracted auth, export, schedule, server, validation - trakt.go (841→185 lines): extracted movies, shows, collections, history - letterboxd.go (941→157 lines): extracted movies, shows, ratings - exports.go (1257→646 lines): extracted processing, csv, cache Also extracted magic numbers into named constants, fixed unchecked errors, replaced deprecated ioutil with os package, and fixed .gitignore pattern that was incorrectly ignoring source files.
1 parent 58e741e commit 8bf093a

28 files changed

+3810
-3626
lines changed

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,9 @@ secrets/
250250
docker-secrets/
251251
.docker-secrets/
252252

253-
# Go specific ignores
254-
export_trakt
255-
export-trakt
253+
# Go specific ignores (binary output only)
254+
/export_trakt
255+
/export-trakt
256256
*.exe
257257
*.exe~
258258
*.dll

cmd/export_trakt/auth.go

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"time"
7+
8+
"github.com/JohanDevl/Export_Trakt_4_Letterboxd/pkg/auth"
9+
"github.com/JohanDevl/Export_Trakt_4_Letterboxd/pkg/config"
10+
"github.com/JohanDevl/Export_Trakt_4_Letterboxd/pkg/logger"
11+
)
12+
13+
// runInteractiveAuth performs interactive OAuth authentication
14+
func runInteractiveAuth(cfg *config.Config, log logger.Logger, tokenManager *auth.TokenManager) error {
15+
oauthMgr := auth.NewOAuthManager(cfg, log)
16+
17+
fmt.Println("🔑 Starting Interactive OAuth Authentication")
18+
fmt.Println("==========================================")
19+
20+
// Check if credentials are configured
21+
if cfg.Trakt.ClientID == "" || cfg.Trakt.ClientSecret == "" {
22+
fmt.Println("❌ Missing Trakt.tv API credentials")
23+
fmt.Println("\nPlease configure your Trakt.tv API credentials:")
24+
fmt.Println("1. Go to https://trakt.tv/oauth/applications")
25+
fmt.Println("2. Create a new application or modify existing one")
26+
fmt.Println("3. Set client_id and client_secret in your config file")
27+
fmt.Printf("4. Set redirect_uri to: %s\n", cfg.Auth.RedirectURI)
28+
return fmt.Errorf("missing API credentials")
29+
}
30+
31+
fmt.Printf("📱 Client ID: %s\n", cfg.Trakt.ClientID)
32+
fmt.Printf("🔗 Redirect URI: %s\n", cfg.Auth.RedirectURI)
33+
34+
// Start local callback server
35+
callbackURL, codeChan, errChan, err := oauthMgr.StartLocalCallbackServer()
36+
if err != nil {
37+
return fmt.Errorf("failed to start callback server: %w", err)
38+
}
39+
40+
fmt.Printf("🌐 Local callback server started at: %s\n", callbackURL)
41+
42+
// Generate authorization URL
43+
authURL, state, err := oauthMgr.GenerateAuthURL()
44+
if err != nil {
45+
return fmt.Errorf("failed to generate auth URL: %w", err)
46+
}
47+
48+
fmt.Println("\n📋 NEXT STEPS:")
49+
fmt.Println("1. Open the following URL in your browser:")
50+
fmt.Printf(" %s\n\n", authURL)
51+
fmt.Println("2. Authorize the application on Trakt.tv")
52+
fmt.Println("3. You will be redirected back automatically")
53+
fmt.Println("\nWaiting for authorization...")
54+
55+
// Wait for authorization code or error
56+
select {
57+
case code := <-codeChan:
58+
fmt.Println("✅ Authorization code received!")
59+
60+
// Exchange code for token
61+
token, err := oauthMgr.ExchangeCodeForToken(code, state, state)
62+
if err != nil {
63+
return fmt.Errorf("failed to exchange code for token: %w", err)
64+
}
65+
66+
// Store token
67+
if err := tokenManager.StoreToken(token); err != nil {
68+
return fmt.Errorf("failed to store token: %w", err)
69+
}
70+
71+
fmt.Println("🎉 Authentication successful!")
72+
fmt.Printf("📅 Token expires: %s\n", oauthMgr.GetTokenExpiryTime(token).Format("2006-01-02 15:04:05"))
73+
fmt.Println("🔄 Automatic refresh is enabled")
74+
75+
return nil
76+
77+
case err := <-errChan:
78+
return fmt.Errorf("authentication error: %w", err)
79+
80+
case <-time.After(5 * time.Minute):
81+
return fmt.Errorf("authentication timeout after 5 minutes")
82+
}
83+
}
84+
85+
// showTokenStatus displays the current token status
86+
func showTokenStatus(tokenManager *auth.TokenManager) error {
87+
fmt.Println("🔍 Token Status Check")
88+
fmt.Println("=====================")
89+
90+
status, err := tokenManager.GetTokenStatus()
91+
if err != nil {
92+
return fmt.Errorf("failed to get token status: %w", err)
93+
}
94+
95+
fmt.Println(status.String())
96+
97+
if status.Error != "" {
98+
fmt.Printf("\n❌ Error: %s\n", status.Error)
99+
}
100+
101+
if status.Message != "" {
102+
fmt.Printf("\n💡 Info: %s\n", status.Message)
103+
}
104+
105+
if !status.HasToken {
106+
fmt.Println("\n🆘 No token found. Run 'auth' command to authenticate:")
107+
fmt.Println(" docker exec -it <container> /app/export-trakt auth")
108+
}
109+
110+
return nil
111+
}
112+
113+
// refreshToken manually refreshes the access token
114+
func refreshToken(tokenManager *auth.TokenManager, log logger.Logger) error {
115+
fmt.Println("🔄 Refreshing Access Token")
116+
fmt.Println("===========================")
117+
118+
// Check current status first
119+
status, err := tokenManager.GetTokenStatus()
120+
if err != nil {
121+
return fmt.Errorf("failed to get token status: %w", err)
122+
}
123+
124+
if !status.HasToken {
125+
fmt.Println("❌ No token to refresh. Run 'auth' command first.")
126+
return fmt.Errorf("no token available")
127+
}
128+
129+
if !status.HasRefreshToken {
130+
fmt.Println("❌ No refresh token available. Re-authentication required.")
131+
fmt.Println("Run: auth")
132+
return fmt.Errorf("no refresh token available")
133+
}
134+
135+
if err := tokenManager.RefreshToken(); err != nil {
136+
return fmt.Errorf("token refresh failed: %w", err)
137+
}
138+
139+
// Show new status
140+
newStatus, err := tokenManager.GetTokenStatus()
141+
if err != nil {
142+
return fmt.Errorf("failed to get new token status: %w", err)
143+
}
144+
145+
fmt.Println("✅ Token refreshed successfully!")
146+
fmt.Printf("📅 New expiry: %s\n", newStatus.ExpiresAt.Format("2006-01-02 15:04:05"))
147+
148+
return nil
149+
}
150+
151+
// clearTokens removes all stored tokens
152+
func clearTokens(tokenManager *auth.TokenManager, log logger.Logger) error {
153+
fmt.Println("🗑️ Clearing Stored Tokens")
154+
fmt.Println("===========================")
155+
156+
if err := tokenManager.ClearToken(); err != nil {
157+
return fmt.Errorf("failed to clear tokens: %w", err)
158+
}
159+
160+
fmt.Println("✅ All tokens cleared successfully!")
161+
fmt.Println("💡 Run 'auth' command to re-authenticate when needed.")
162+
163+
return nil
164+
}
165+
166+
// showAuthURL generates and displays the OAuth authentication URL
167+
func showAuthURL(cfg *config.Config, log logger.Logger) error {
168+
oauthMgr := auth.NewOAuthManager(cfg, log)
169+
170+
fmt.Println("🔗 OAuth Authentication URL Generator")
171+
fmt.Println("=====================================")
172+
173+
// Check if credentials are configured
174+
if cfg.Trakt.ClientID == "" || cfg.Trakt.ClientSecret == "" {
175+
fmt.Println("❌ Missing Trakt.tv API credentials")
176+
fmt.Println("\nPlease configure your Trakt.tv API credentials:")
177+
fmt.Println("1. Go to https://trakt.tv/oauth/applications")
178+
fmt.Println("2. Create a new application or modify existing one")
179+
fmt.Println("3. Set client_id and client_secret in your config file")
180+
fmt.Printf("4. Set redirect_uri to: %s\n", cfg.Auth.RedirectURI)
181+
return fmt.Errorf("missing API credentials")
182+
}
183+
184+
fmt.Printf("📱 Client ID: %s\n", cfg.Trakt.ClientID)
185+
fmt.Printf("🔗 Redirect URI: %s\n", cfg.Auth.RedirectURI)
186+
187+
// Generate authorization URL
188+
authURL, state, err := oauthMgr.GenerateAuthURL()
189+
if err != nil {
190+
return fmt.Errorf("failed to generate auth URL: %w", err)
191+
}
192+
193+
fmt.Println("\n🚀 AUTHENTICATION STEPS:")
194+
fmt.Println("1. Copy and open this URL in your browser:")
195+
fmt.Printf(" %s\n\n", authURL)
196+
fmt.Println("2. Authorize the application on Trakt.tv")
197+
fmt.Println("3. You will be redirected to localhost - this is normal")
198+
fmt.Println("4. Copy the 'code' parameter from the URL")
199+
fmt.Println("5. Run the interactive auth command:")
200+
fmt.Printf(" docker run --rm -v \"$(pwd)/config:/app/config\" -p %d:%d trakt-exporter auth\n", cfg.Auth.CallbackPort, cfg.Auth.CallbackPort)
201+
fmt.Println("\n💾 State (for security):", state)
202+
fmt.Println("\n💡 This URL is valid for 10 minutes.")
203+
204+
return nil
205+
}
206+
207+
// authenticateWithCode performs OAuth authentication using a provided authorization code
208+
func authenticateWithCode(cfg *config.Config, log logger.Logger, tokenManager *auth.TokenManager, authCode string) error {
209+
oauthMgr := auth.NewOAuthManager(cfg, log)
210+
211+
fmt.Println("🔑 Manual OAuth Authentication with Code")
212+
fmt.Println("=========================================")
213+
214+
// Check if credentials are configured
215+
if cfg.Trakt.ClientID == "" || cfg.Trakt.ClientSecret == "" {
216+
fmt.Println("❌ Missing Trakt.tv API credentials")
217+
fmt.Println("\nPlease configure your Trakt.tv API credentials in config.toml")
218+
return fmt.Errorf("missing API credentials")
219+
}
220+
221+
fmt.Printf("📱 Client ID: %s\n", cfg.Trakt.ClientID)
222+
fmt.Printf("🔗 Redirect URI: %s\n", cfg.Auth.RedirectURI)
223+
fmt.Printf("🔐 Authorization Code: %s\n", authCode)
224+
225+
// Generate a state for this manual authentication (not validated since we're not using callback)
226+
_, state, err := oauthMgr.GenerateAuthURL()
227+
if err != nil {
228+
return fmt.Errorf("failed to generate state: %w", err)
229+
}
230+
231+
fmt.Println("\n🔄 Exchanging authorization code for tokens...")
232+
233+
// Exchange code for token (we'll use the generated state)
234+
token, err := oauthMgr.ExchangeCodeForToken(authCode, state, state)
235+
if err != nil {
236+
fmt.Printf("❌ Token exchange failed: %s\n", err.Error())
237+
fmt.Println("\n💡 Possible reasons:")
238+
fmt.Println(" - Authorization code has expired (they expire quickly)")
239+
fmt.Println(" - Authorization code has already been used")
240+
fmt.Println(" - Redirect URI mismatch in Trakt.tv app settings")
241+
fmt.Printf(" - Expected redirect URI: %s\n", cfg.Auth.RedirectURI)
242+
return err
243+
}
244+
245+
// Store the token
246+
if err := tokenManager.StoreToken(token); err != nil {
247+
fmt.Printf("❌ Failed to store token: %s\n", err.Error())
248+
return err
249+
}
250+
251+
fmt.Println("✅ Authentication successful!")
252+
fmt.Printf("📅 Token expires: %s\n", oauthMgr.GetTokenExpiryTime(token).Format("2006-01-02 15:04:05"))
253+
fmt.Println("🔄 Automatic refresh is enabled")
254+
fmt.Println("\n💡 You can now run export commands normally.")
255+
256+
return nil
257+
}
258+
259+
// fixCredentialsPermissions fixes file permissions for credentials storage
260+
func fixCredentialsPermissions(cfg *config.Config, log logger.Logger) error {
261+
credentialsPath := "./config/credentials.enc"
262+
263+
fmt.Printf("🔧 Fixing credentials file permissions...\n\n")
264+
265+
// Check if file exists
266+
info, err := os.Stat(credentialsPath)
267+
if err != nil {
268+
if os.IsNotExist(err) {
269+
fmt.Printf("✅ Credentials file doesn't exist yet - no action needed.\n")
270+
fmt.Printf(" File will be created with proper permissions when you authenticate.\n")
271+
return nil
272+
}
273+
return fmt.Errorf("failed to check credentials file: %w", err)
274+
}
275+
276+
currentMode := info.Mode()
277+
fmt.Printf("📋 Current file permissions: %o\n", currentMode&os.ModePerm)
278+
279+
// Check Docker environment
280+
isDocker := false
281+
if _, err := os.Stat("/.dockerenv"); err == nil {
282+
isDocker = true
283+
fmt.Printf("🐳 Detected Docker environment\n")
284+
}
285+
286+
// Determine target permissions
287+
targetMode := os.FileMode(0600)
288+
if isDocker {
289+
// In Docker, we might need more relaxed permissions
290+
if currentMode&0077 != 0 && currentMode&0044 == 0 {
291+
fmt.Printf("✅ File permissions are acceptable for Docker environment.\n")
292+
return nil
293+
}
294+
// Try to set more restrictive permissions, but accept failure in Docker
295+
targetMode = os.FileMode(0644)
296+
}
297+
298+
fmt.Printf("🎯 Target permissions: %o\n", targetMode)
299+
300+
// Try to change permissions
301+
if err := os.Chmod(credentialsPath, targetMode); err != nil {
302+
if isDocker {
303+
fmt.Printf("⚠️ Warning: Could not change file permissions in Docker environment.\n")
304+
fmt.Printf(" This is normal - Docker handles file permissions differently.\n")
305+
fmt.Printf(" Your credentials should still work properly.\n")
306+
return nil
307+
}
308+
return fmt.Errorf("failed to change file permissions: %w", err)
309+
}
310+
311+
// Verify the change
312+
newInfo, err := os.Stat(credentialsPath)
313+
if err != nil {
314+
return fmt.Errorf("failed to verify permissions change: %w", err)
315+
}
316+
317+
newMode := newInfo.Mode()
318+
fmt.Printf("✅ Permissions updated successfully: %o\n", newMode&os.ModePerm)
319+
320+
fmt.Printf("\n💡 Tips:\n")
321+
fmt.Printf(" - If you're still having issues, try using the 'env' keyring backend\n")
322+
fmt.Printf(" - Set TRAKT_CLIENT_ID and TRAKT_CLIENT_SECRET environment variables\n")
323+
fmt.Printf(" - Update config.toml: keyring_backend = \"env\"\n")
324+
325+
return nil
326+
}

0 commit comments

Comments
 (0)