Skip to content

Commit 2733a4d

Browse files
authored
Feature/task logging (#59)
* base file * task logging update * c u * logs * logs v2 * g e t l o g s * tests boi * fix migration
1 parent 72e24ee commit 2733a4d

File tree

15 files changed

+510
-159
lines changed

15 files changed

+510
-159
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import requests
2+
import json
3+
import time
4+
5+
# === CONFIGURATION ===
6+
SPOTIFY_ACCESS_TOKEN = ""
7+
YOUTUBE_ACCESS_TOKEN = ""
8+
SPOTIFY_PLAYLIST_ID = "" # Only the ID, not full URL
9+
MAX_SONGS = 100 # Adjust as needed
10+
11+
SPOTIFY_API_BASE = "https://api.spotify.com/v1"
12+
YOUTUBE_API_BASE = "https://www.googleapis.com/youtube/v3"
13+
14+
# === STEP 1: Get Songs from a Spotify Playlist ===
15+
def get_spotify_playlist_songs(playlist_id, max_songs=None):
16+
headers = {"Authorization": f"Bearer {SPOTIFY_ACCESS_TOKEN}"}
17+
url = f"{SPOTIFY_API_BASE}/playlists/{playlist_id}/tracks?limit=100"
18+
songs = []
19+
20+
while url:
21+
response = requests.get(url, headers=headers)
22+
if response.status_code != 200:
23+
print(f"Failed to fetch playlist songs: {response.status_code}{response.text}")
24+
break
25+
26+
data = response.json()
27+
for item in data.get('items', []):
28+
track = item.get('track')
29+
if track:
30+
name = track['name']
31+
artists = ', '.join(artist['name'] for artist in track['artists'])
32+
query = f"{artists} {name}"
33+
songs.append(query)
34+
35+
# Optional cap
36+
if max_songs and len(songs) >= max_songs:
37+
return songs
38+
39+
url = data.get('next') # Spotify handles pagination via `next`
40+
time.sleep(0.1) # Throttle to be API-friendly
41+
42+
return songs
43+
44+
# === STEP 2: Search YouTube Video ===
45+
def search_youtube_video(query):
46+
headers = {"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}"}
47+
params = {
48+
'part': 'snippet',
49+
'q': query,
50+
'maxResults': 1,
51+
'type': 'video'
52+
}
53+
54+
response = requests.get(f"{YOUTUBE_API_BASE}/search", headers=headers, params=params)
55+
if response.status_code != 200:
56+
print(f"Failed YouTube search: {response.status_code}{query}")
57+
return None
58+
59+
items = response.json().get('items', [])
60+
if items:
61+
return items[0]['id']['videoId']
62+
return None
63+
64+
# === STEP 3: Create YouTube Playlist ===
65+
def create_youtube_playlist(title, description="Imported from Spotify Playlist"):
66+
headers = {
67+
"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}",
68+
"Content-Type": "application/json"
69+
}
70+
payload = {
71+
"snippet": {
72+
"title": title,
73+
"description": description
74+
},
75+
"status": {
76+
"privacyStatus": "private"
77+
}
78+
}
79+
80+
response = requests.post(
81+
f"{YOUTUBE_API_BASE}/playlists?part=snippet,status",
82+
headers=headers,
83+
data=json.dumps(payload)
84+
)
85+
86+
if response.status_code != 200:
87+
print(f"Failed to create playlist: {response.status_code}{response.text}")
88+
return None
89+
90+
return response.json().get("id")
91+
92+
# === STEP 4: Add Video to YouTube Playlist ===
93+
def add_video_to_playlist(video_id, playlist_id):
94+
headers = {
95+
"Authorization": f"Bearer {YOUTUBE_ACCESS_TOKEN}",
96+
"Content-Type": "application/json"
97+
}
98+
payload = {
99+
"snippet": {
100+
"playlistId": playlist_id,
101+
"resourceId": {
102+
"kind": "youtube#video",
103+
"videoId": video_id
104+
}
105+
}
106+
}
107+
108+
response = requests.post(
109+
f"{YOUTUBE_API_BASE}/playlistItems?part=snippet",
110+
headers=headers,
111+
data=json.dumps(payload)
112+
)
113+
114+
if response.status_code not in (200, 201):
115+
print(f"Failed to add video {video_id} to playlist: {response.status_code}")
116+
return False
117+
118+
return True
119+
120+
# === MAIN EXECUTION ===
121+
def main():
122+
print("▶ Fetching songs from Spotify playlist...")
123+
songs = get_spotify_playlist_songs(SPOTIFY_PLAYLIST_ID)
124+
print(f"🎵 Found {len(songs)} songs in playlist.")
125+
with open("playlist.txt", "a") as f:
126+
for line in songs:
127+
f.write(f"{line}\n")
128+
# print("📺 Creating YouTube playlist...")
129+
# playlist_title = "Spotify Playlist to YouTube"
130+
# playlist_id = create_youtube_playlist(playlist_title)
131+
# if not playlist_id:
132+
# print("❌ Could not create YouTube playlist.")
133+
# return
134+
135+
print("🔁 Searching on YouTube and adding to playlist...")
136+
for idx, song in enumerate(songs, 1):
137+
print(f"[{idx}/{len(songs)}] Searching: {song}")
138+
# video_id = search_youtube_video(song)
139+
# if video_id:
140+
# if add_video_to_playlist(video_id, playlist_id):
141+
# print(" ✅ Added successfully")
142+
# else:
143+
# print(" ❌ Failed to add")
144+
# else:
145+
# print(" ❌ No video found")
146+
# time.sleep(1.1) # Be gentle with YouTube's API
147+
148+
print("✅ Done!")
149+
150+
if __name__ == "__main__":
151+
main()
152+

MyMusicBoxApi/database/db.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ const ReturningIdParameter = "RETURNING"
2020
const ReturningIdParameterLower = "returning"
2121
const DatabaseDriver = "postgres"
2222
const MigrationFolder = "migration_scripts"
23-
const MaxOpenConnections = 10
24-
const MaxIdleConnections = 5
23+
const MaxOpenConnections = 15
24+
const MaxIdleConnections = 10
2525
const MaxConnectionIdleTimeInMinutes = 10
2626
const MaxConnectionLifeTimeInMinutes = 10
2727

@@ -164,7 +164,7 @@ func (base *BaseTable) QueryRows(query string) (*sql.Rows, error) {
164164
}
165165

166166
func ApplyMigrations() {
167-
logging.Info("Applying migrations...")
167+
logging.Info("Checking for database migration files")
168168
// files will be sorted by filename
169169
// to make sure the migrations are executed in order
170170
// this naming convention must be used

MyMusicBoxApi/database/migrationtable.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,22 @@ func (table *MigrationTable) Insert(filename string, contents string) (err error
3030
}
3131

3232
func (table *MigrationTable) ApplyMigration(query string) (err error) {
33-
return table.NonScalarQuery(query)
33+
34+
transaction, err := table.DB.Begin()
35+
36+
if err != nil {
37+
logging.Error(fmt.Sprintf("Failed to creare transaction %s", err.Error()))
38+
return
39+
}
40+
41+
_, err = transaction.Exec(query)
42+
43+
if err != nil {
44+
logging.Error(fmt.Sprintf("Failed to execute query %s", err.Error()))
45+
return
46+
}
47+
48+
return transaction.Commit()
3449
}
3550

3651
func (table *MigrationTable) GetCurrentAppliedMigrationFileName() (fileName string, err error) {

MyMusicBoxApi/database/migrationtable_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ func TestApplyMigration(t *testing.T) {
6464
query := "DROP DATABSE migration"
6565

6666
mock.ExpectBegin()
67-
mock.ExpectPrepare(query)
6867
mock.ExpectExec(query).
6968
WillReturnResult(sqlmock.NewResult(1, 1))
7069
mock.ExpectCommit()

MyMusicBoxApi/database/tasklogtable.go

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ package database
22

33
import (
44
"context"
5-
"fmt"
65
"musicboxapi/logging"
76
"musicboxapi/models"
8-
"time"
97
)
108

119
type ITasklogTable interface {
12-
InsertTaskLog() (lastInsertedId int, err error)
13-
UpdateTaskLogStatus(taskId int, nStatus int) (err error)
14-
EndTaskLog(taskId int, nStatus int, data []byte) (err error)
15-
UpdateTaskLogError(params ...any) (err error)
16-
GetTaskLogs(ctx context.Context) ([]models.TaskLog, error)
10+
GetParentLogs(ctx context.Context) ([]models.ParentTaskLog, error)
11+
GetChildLogs(ctx context.Context, parentId int) ([]models.ChildTaskLog, error)
12+
CreateParentTaskLog(url string) (models.ParentTaskLog, error)
13+
CreateChildTaskLog(parent models.ParentTaskLog) (models.ChildTaskLog, error)
14+
UpdateChildTaskLogStatus(child models.ChildTaskLog) error
15+
ChildTaskLogDone(child models.ChildTaskLog) error
16+
ChildTaskLogError(child models.ChildTaskLog) error
1717
}
1818

1919
type TasklogTable struct {
@@ -25,59 +25,108 @@ func NewTasklogTableInstance() *TasklogTable {
2525
BaseTable: NewBaseTableInstance(),
2626
}
2727
}
28+
func (table *TasklogTable) GetParentLogs(ctx context.Context) ([]models.ParentTaskLog, error) {
29+
query := "SELECT * FROM ParentTaskLog ORDER BY AddTime desc"
2830

29-
func (table *TasklogTable) InsertTaskLog() (lastInsertedId int, error error) {
30-
query := `INSERT INTO TaskLog (Status) VALUES($1) RETURNING Id`
31+
rows, err := table.QueryRowsContex(ctx, query)
3132

32-
lastInsertedId, err := table.InsertWithReturningId(query, int(models.Pending))
33+
if err != nil {
34+
return make([]models.ParentTaskLog, 0), nil
35+
}
3336

34-
return lastInsertedId, err
35-
}
37+
defer rows.Close()
3638

37-
func (table *TasklogTable) UpdateTaskLogStatus(taskId int, nStatus int) (error error) {
38-
query := `UPDATE TaskLog SET Status = $1 WHERE Id = $2`
39+
var parentLog models.ParentTaskLog
40+
logs := make([]models.ParentTaskLog, 0)
3941

40-
return table.NonScalarQuery(query, nStatus, taskId)
41-
}
42+
for rows.Next() {
43+
err := rows.Scan(&parentLog.Id, &parentLog.Url, &parentLog.AddTime)
4244

43-
func (table *TasklogTable) EndTaskLog(taskId int, nStatus int, data []byte) error {
44-
query := `UPDATE TaskLog SET Status = $1, OutputLog = $2, EndTime = $3 WHERE Id = $4`
45+
if err != nil {
46+
logging.ErrorStackTrace(err)
47+
continue
48+
}
4549

46-
return table.NonScalarQuery(query, nStatus, data, time.Now(), taskId)
47-
}
50+
logs = append(logs, parentLog)
51+
}
4852

49-
func (table *TasklogTable) UpdateTaskLogError(params ...any) error {
50-
query := `UPDATE TaskLog
51-
SET Status = $1, OutputLog = $2, EndTime = $3
52-
WHERE Id = $4`
53-
return table.NonScalarQuery(query, params...)
53+
return logs, nil
5454
}
55+
func (table *TasklogTable) GetChildLogs(ctx context.Context, parentId int) ([]models.ChildTaskLog, error) {
56+
query := "SELECT * FROM ChildTaskLog WHERE ParentId = $1 ORDER BY StartTime desc"
5557

56-
func (table *TasklogTable) GetTaskLogs(ctx context.Context) ([]models.TaskLog, error) {
57-
query := `SELECT Id, StartTime, EndTime, Status, OutputLog FROM TaskLog ORDER BY Id desc` // get the latest first
58-
59-
rows, err := table.QueryRowsContex(ctx, query)
58+
rows, err := table.QueryRowsContex(ctx, query, parentId)
6059

6160
if err != nil {
62-
logging.Error(fmt.Sprintf("QueryRow error: %s", err.Error()))
63-
return nil, err
61+
return make([]models.ChildTaskLog, 0), nil
6462
}
65-
defer rows.Close()
6663

67-
var tasklog models.TaskLog
64+
defer rows.Close()
6865

69-
tasks := make([]models.TaskLog, 0)
66+
var childLog models.ChildTaskLog
67+
logs := make([]models.ChildTaskLog, 0)
7068

7169
for rows.Next() {
72-
scanError := rows.Scan(&tasklog.Id, &tasklog.StartTime, &tasklog.EndTime, &tasklog.Status, &tasklog.OutputLog)
70+
err := rows.Scan(&childLog.Id, &childLog.ParentId, &childLog.StartTime, &childLog.EndTime, &childLog.Status, &childLog.OutputLog)
7371

74-
if scanError != nil {
75-
logging.Error(fmt.Sprintf("Scan error: %s", scanError.Error()))
72+
if err != nil {
73+
logging.ErrorStackTrace(err)
7674
continue
7775
}
7876

79-
tasks = append(tasks, tasklog)
77+
logs = append(logs, childLog)
78+
}
79+
80+
return logs, nil
81+
}
82+
func (table *TasklogTable) CreateParentTaskLog(url string) (models.ParentTaskLog, error) {
83+
query := "INSERT INTO ParentTaskLog (Url) Values($1) RETURNING Id"
84+
85+
id, err := table.InsertWithReturningId(query, url)
86+
87+
if err != nil {
88+
return models.ParentTaskLog{}, err
89+
}
90+
91+
return models.ParentTaskLog{
92+
Id: id,
93+
Url: url,
94+
}, nil
95+
}
96+
func (table *TasklogTable) CreateChildTaskLog(parent models.ParentTaskLog) (models.ChildTaskLog, error) {
97+
query := "INSERT INTO ChildTaskLog (ParentId, Status) VALUES($1,$2) RETURNING Id"
98+
99+
defaultStatus := int(models.Pending)
100+
101+
id, err := table.InsertWithReturningId(query, parent.Id, defaultStatus)
102+
103+
if err != nil {
104+
return models.ChildTaskLog{}, err
80105
}
81106

82-
return tasks, nil
107+
return models.ChildTaskLog{
108+
Id: id,
109+
ParentId: parent.Id,
110+
Status: defaultStatus,
111+
}, nil
112+
}
113+
func (table *TasklogTable) UpdateChildTaskLogStatus(child models.ChildTaskLog) error {
114+
115+
if child.Status == int(models.Downloading) {
116+
// set the start time to now
117+
query := "UPDATE ChildTaskLog SET StartTime = CURRENT_TIMESTAMP, Status = $1 WHERE Id = $2"
118+
return table.NonScalarQuery(query, child.Status, child.Id)
119+
} else {
120+
// just update
121+
query := "UPDATE ChildTaskLog SET Status = $1 WHERE Id = $2"
122+
return table.NonScalarQuery(query, child.Status, child.Id)
123+
}
124+
}
125+
func (table *TasklogTable) ChildTaskLogDone(child models.ChildTaskLog) error {
126+
query := "UPDATE ChildTaskLog SET Status = $1, OutputLog = $2, EndTime = CURRENT_TIMESTAMP WHERE Id = $3"
127+
return table.NonScalarQuery(query, int(models.Done), child.OutputLog, child.Id)
128+
}
129+
func (table *TasklogTable) ChildTaskLogError(child models.ChildTaskLog) error {
130+
query := "UPDATE ChildTaskLog SET Status = $1, OutputLog = $2, EndTime = CURRENT_TIMESTAMP WHERE Id = $3"
131+
return table.NonScalarQuery(query, int(models.Error), child.OutputLog, child.Id)
83132
}

MyMusicBoxApi/http/download.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package http
22

33
import (
4-
"musicboxapi/database"
54
"musicboxapi/models"
65
"musicboxapi/service"
76
"net/http"
@@ -25,15 +24,15 @@ func DownloadRequest(ctx *gin.Context) {
2524
return
2625
}
2726

28-
tasklogTable := database.NewTasklogTableInstance()
27+
//tasklogTable := database.NewTasklogTableInstance()
2928
// Insert a new task
30-
taskId, err := tasklogTable.InsertTaskLog()
29+
// parentTask, err := tasklogTable.CreateParentTaskLog(request.Url)
3130

3231
if err != nil {
3332
ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err))
3433
return
3534
}
3635

37-
go service.StartDownloadTask(taskId, request)
38-
ctx.JSON(http.StatusOK, models.OkResponse(gin.H{"taskId": taskId}, "Started task"))
36+
go service.StartDownloadTask(request)
37+
ctx.JSON(http.StatusOK, models.OkResponse(gin.H{"": ""}, "Created"))
3938
}

0 commit comments

Comments
 (0)