diff --git a/app.go b/app.go
new file mode 100644
index 0000000..d3cbf91
--- /dev/null
+++ b/app.go
@@ -0,0 +1,430 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "time"
+
+ . "ytd/constants"
+ . "ytd/db"
+ . "ytd/models"
+ . "ytd/plugins"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options/dialog"
+ "github.com/xujiajun/nutsdb"
+
+ tb "gopkg.in/tucnak/telebot.v2"
+)
+
+var wailsRuntime *wails.Runtime
+
+type AppState struct {
+ runtime *wails.Runtime
+ db *nutsdb.DB
+ plugins []Plugin
+ Entries []GenericEntry `json:"entries"`
+ Config *AppConfig `json:"config"`
+ Stats *AppStats
+}
+
+func (state *AppState) WailsInit(runtime *wails.Runtime) {
+ // Save runtime
+ state.runtime = runtime
+ // Do some other initialisation
+ state.db = InitializeDb()
+ state.Entries = DbGetAllEntries()
+ state.Config = state.Config.Init()
+ state.Stats = &AppStats{}
+ appState = state
+
+ // this is sync so it blocks until finished and wails:loaded are not dispatched until this finishes
+ if runtime.System.AppType() == "default" { // wails serve & ng serve
+ runtime.Events.On("wails:loaded", func(...interface{}) {
+ time.Sleep(100 * time.Millisecond)
+ fmt.Println("EMIT YTD:ONLOAD")
+ runtime.Events.Emit("ytd:onload", state)
+ })
+ } else { // dekstop build
+ go func() {
+ runtime.Events.Emit("ytd:onload", state)
+ }()
+ }
+
+ for _, plugin := range plugins {
+ plugin.SetWailsRuntime(runtime)
+ plugin.SetAppConfig(state.Config)
+ plugin.SetAppStats(state.Stats)
+ }
+ fmt.Println("APP STATE INITIALIZED")
+
+ /* go func() {
+ for {
+ time.Sleep(10 * time.Second)
+ state.checkForTracksToDownload()
+ }
+ }() */
+
+ /* go func() {
+ for {
+ restart := make(chan int)
+ time.Sleep(3 * time.Second)
+ go state.telegramShareTracks(restart)
+
+ for {
+ select {
+ case <-restart:
+ fmt.Println("Share tracks...")
+ go state.telegramShareTracks(restart)
+ }
+
+ }
+ }
+ }() */
+
+ go func() {
+ restart := make(chan int)
+ time.Sleep(3 * time.Second)
+ go state.convertToMp3(restart)
+
+ for {
+ select {
+ case <-restart:
+ fmt.Println("Restart converting...")
+ go state.convertToMp3(restart)
+ }
+
+ }
+ }()
+}
+
+func (s *WailsRuntime) WailsShutdown() {
+ CloseDb()
+}
+
+func (state *AppState) GetAppConfig() *AppConfig {
+ return state.Config
+}
+
+func (state *AppState) SelectDirectory() (string, error) {
+ selectedDirectory, err := state.runtime.Dialog.OpenDirectory(&dialog.OpenDialog{
+ AllowFiles: false,
+ CanCreateDirectories: true,
+ AllowDirectories: true,
+ Title: "Choose directory",
+ })
+ return selectedDirectory, err
+}
+
+func (state *AppState) GetEntryById(entry GenericEntry) *GenericEntry {
+ for _, t := range state.Entries {
+ if t.Type == "track" && t.Track.ID == entry.Track.ID {
+ return &t
+ }
+ }
+ return nil
+}
+
+func (state *AppState) checkForTracksToDownload() error {
+ fmt.Printf("Check for tracks to start downloads...%d/%d\n", state.Stats.DownloadingCount, state.Config.MaxParrallelDownloads)
+ if state.Stats.DownloadingCount >= state.Config.MaxParrallelDownloads {
+ return nil
+ }
+
+ fmt.Printf("Checking...%d entries\n", len(state.Entries))
+ // range over DbGetAllEntries()
+ for _, t := range DbGetAllEntries() {
+ var freeSlots uint = state.Config.MaxParrallelDownloads - state.Stats.DownloadingCount
+ entry := t
+ // auto download only tracks with processing status
+ // if track has pending/failed status it means that something goes wrong so user have to download it manually from UI
+ if entry.Type == "track" && entry.Track.Status == TrackStatusProcessing {
+ if freeSlots == 0 {
+ return nil
+ }
+
+ // start download for track
+ fmt.Printf("Found %s to download\n", entry.Track.Name)
+ plugin := getPluginFor(entry.Source)
+ // make chan GenericEntry
+ // goriutine scrive li dentro
+ if plugin != nil {
+ freeSlots--
+ go func(entry GenericEntry) {
+ // storedEntry := state.GetEntryById(entry)
+ // fmt.Printf("Stored entry %v\n", storedEntry)
+ // fmt.Println(entry.Track.Url)
+ // fmt.Println(storedEntry.Track.Url)
+ plugin.StartDownload(&entry)
+ // storedEntry.Track = entry.Track
+ }(entry)
+ }
+ }
+ }
+
+ // qui un for che legge dal channel e ogni volta che riceve una entry downloadata
+ return nil
+}
+
+type recipientString string
+
+func (r recipientString) Recipient() string {
+ return r.Recipient()
+}
+
+func (state *AppState) telegramShareTracks(restart chan<- int) error {
+ if !state.Config.Telegram.Share {
+ // if option is not enabled restart check after 30s
+ time.Sleep(30 * time.Second)
+ restart <- 1
+ return nil
+ }
+
+ b, err := tb.NewBot(tb.Settings{
+ Token: "1903196088:AAHhWGvfhQfS_MlhvohFvQYnrg3z7GsBPOM",
+ Poller: &tb.LongPoller{Timeout: 10 * time.Second},
+ })
+
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+
+ audio := &tb.Audio{File: tb.FromDisk("/Users/oskarmarciniak/songs/youtube/2vOU4nVI_DA.mp3"), FileName: "Track name", Title: "Track name title", Duration: 207, Caption: "Caption tracks"}
+ b.Send(&tb.Chat{ID: 903612486}, audio)
+
+ b.Handle("/hello", func(m *tb.Message) {
+ fmt.Println(m.Chat)
+ b.Send(m.Sender, "Hello World!")
+ // cercare il primo msg che come username ha quello impostato nell'app e salvarsi la chat_id
+ })
+
+ b.Start()
+
+ return nil
+}
+
+func (state *AppState) convertToMp3(restart chan<- int) error {
+ if !state.Config.ConvertToMp3 {
+ // if option is not enabled restart check after 30s
+ time.Sleep(30 * time.Second)
+ restart <- 1
+ return nil
+ }
+
+ fmt.Println("Converting....")
+ ffmpeg, _ := state.IsFFmpegInstalled()
+
+ for _, t := range DbGetAllEntries() {
+ entry := t
+ plugin := getPluginFor(entry.Source)
+
+ if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") {
+ fmt.Printf("Extracting audio for %s...\n", entry.Track.Name)
+
+ // ffmpeg -i "41qC3w3UUkU.webm" -vn -ab 128k -ar 44100 -y "41qC3w3UUkU.mp3"
+ cmd := exec.Command(
+ ffmpeg,
+ "-loglevel", "quiet",
+ "-i", fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID),
+ "-vn",
+ "-ab", "128k",
+ "-ar", "44100",
+ "-y", fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID),
+ )
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ if err := cmd.Run(); err != nil {
+ fmt.Println("Failed to extract audio:", err)
+ } else {
+ entry.Track.IsConvertedToMp3 = true
+ DbWriteEntry(entry.Track.ID, entry)
+ state.runtime.Events.Emit("ytd:track", entry) // track:converted:mp3
+
+ // remove webm
+ if state.Config.CleanWebmFiles && plugin.IsTrackFileExists(entry.Track, "webm") {
+ err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
+ if err != nil && !os.IsNotExist(err) {
+ fmt.Printf("Cannot remove %s.webm file after successfull converting to mp3\n", entry.Track.ID)
+ }
+ }
+ }
+ restart <- 1
+ return nil
+ }
+ }
+
+ // if there are no tracks to convert delay between restart
+ time.Sleep(30 * time.Second)
+ restart <- 1
+ return nil
+}
+
+func (state *AppState) SaveSettingBoolValue(name string, val bool) (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("Recovering from panic saveSettingValue:", r)
+ switch x := r.(type) {
+ case string:
+ err = errors.New(x)
+ case error:
+ err = x
+ default:
+ // Fallback err (per specs, error strings should be lowercase w/o punctuation
+ err = errors.New("unknown panic")
+ }
+ }
+ }()
+
+ error := DbSaveSettingBoolValue(name, val)
+ if err != nil {
+ return error
+ }
+
+ appState.Config.Set(name, val)
+ return nil
+}
+
+func (state *AppState) SaveSettingValue(name string, val string) (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("Recovering from panic saveSettingValue:", r)
+ switch x := r.(type) {
+ case string:
+ err = errors.New(x)
+ case error:
+ err = x
+ default:
+ // Fallback err (per specs, error strings should be lowercase w/o punctuation
+ err = errors.New("unknown panic")
+ }
+ }
+ }()
+
+ error := DbWriteSetting(name, val)
+ if err != nil {
+ return error
+ }
+
+ appState.Config.Set(name, val)
+ return nil
+}
+
+func (state *AppState) ReadSettingBoolValue(name string) (bool, error) {
+ return DbReadSettingBoolValue(name)
+}
+
+func (state *AppState) ReadSettingValue(name string) (string, error) {
+ return DbReadSetting(name)
+}
+
+func (state *AppState) RemoveEntry(record map[string]interface{}) error {
+ var err error
+ var entry GenericEntry
+ err = mapstructure.Decode(record, &entry)
+ if err != nil {
+ return err
+ }
+
+ if entry.Type == "track" {
+ if err = DbDeleteEntry(entry.Track.ID); err == nil {
+ plugin := getPluginFor(entry.Source)
+ if plugin.IsTrackFileExists(entry.Track, "webm") {
+ err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
+ fmt.Println(err)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ // remove mp3 if file has been already converted
+ if plugin.IsTrackFileExists(entry.Track, "mp3") {
+ err = os.Remove(fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID))
+ fmt.Println(err)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ }
+ }
+ return err
+}
+
+func (state *AppState) AddToDownload(url string, isFromClipboard bool) error {
+ for _, plugin := range plugins {
+ if support := plugin.Supports(url); support {
+ if appState.GetAppConfig().ConcurrentDownloads {
+ go func() {
+ plugin.Fetch(url, isFromClipboard)
+ }()
+ } else {
+ plugin.Fetch(url, isFromClipboard)
+ }
+ continue
+ }
+ }
+ return nil
+}
+
+func (state *AppState) StartDownload(record map[string]interface{}) error {
+ var err error
+ var entry GenericEntry
+ err = mapstructure.Decode(record, &entry)
+ if err != nil {
+ return err
+ }
+
+ if appState.Stats.DownloadingCount >= appState.Config.MaxParrallelDownloads {
+ return errors.New("Max simultaneous downloads are reached please retry after some track finished downloading")
+ }
+
+ for _, plugin := range plugins {
+ if plugin.GetName() == entry.Source && entry.Type == "track" {
+ if appState.GetAppConfig().ConcurrentDownloads {
+ go func() {
+ plugin.StartDownload(&entry)
+ }()
+ } else {
+ plugin.StartDownload(&entry)
+ }
+ continue
+ }
+ }
+ return nil
+}
+
+func (state *AppState) IsSupportedUrl(url string) bool {
+ for _, plugin := range plugins {
+ if support := plugin.Supports(url); support {
+ return true
+ }
+ }
+ return false
+}
+
+func (state *AppState) IsFFmpegInstalled() (string, error) {
+ ffmpeg, err := exec.LookPath("ffmpeg")
+ return ffmpeg, err
+}
+
+func (state *AppState) OpenUrl(url string) error {
+ return state.runtime.Browser.Open(url)
+}
+
+func getPluginFor(name string) Plugin {
+ for _, plugin := range plugins {
+ if plugin.GetName() == name {
+ return plugin
+ }
+ }
+ return nil
+}
+
+//WailsRuntime .
+type WailsRuntime struct {
+ runtime *wails.Runtime
+}
diff --git a/frontend/src/app/components/audio-player/audio-player.component.ts b/frontend/src/app/components/audio-player/audio-player.component.ts
index b5606c5..44d5072 100644
--- a/frontend/src/app/components/audio-player/audio-player.component.ts
+++ b/frontend/src/app/components/audio-player/audio-player.component.ts
@@ -71,7 +71,7 @@ export class AudioPlayerComponent implements OnInit {
}
private _play(track: Track): void {
- const src = track.isConvertedToMp3 ? `http://localhost:8080/youtube/${track.id}.mp3` : `http://localhost:8080/youtube/${track.id}.webm`;
+ const src = track.isConvertedToMp3 ? `http://localhost:8080/tracks/youtube/${track.id}.mp3` : `http://localhost:8080/tracks/youtube/${track.id}.webm`;
this.audio = new Audio(src);
this.audio.ontimeupdate = (e) => {
diff --git a/frontend/src/app/components/settings/settings.component.html b/frontend/src/app/components/settings/settings.component.html
index 9923620..dc7e3f5 100644
--- a/frontend/src/app/components/settings/settings.component.html
+++ b/frontend/src/app/components/settings/settings.component.html
@@ -56,14 +56,15 @@
Share downloaded tracks with your devices through telegram
-
diff --git a/frontend/src/app/components/settings/settings.component.scss b/frontend/src/app/components/settings/settings.component.scss
index f78af2c..a52f34d 100644
--- a/frontend/src/app/components/settings/settings.component.scss
+++ b/frontend/src/app/components/settings/settings.component.scss
@@ -20,10 +20,26 @@ settings {
background: transparent;
}
+ .open-link {
+ font-weight: bold;
+ text-decoration: underline;
+ cursor: pointer;
+ }
+
+ .at {
+ font-weight: bold;
+ padding-right: 8px;
+ }
+
.toggle-wrapper {
+ margin-left: 46px;
+
+ .mat-slide-toggle {
+ margin-left: -46px;
+ }
+
.warning {
font-weight: bold;
- padding-left: 44px;
}
}
}
diff --git a/frontend/src/app/components/settings/settings.component.ts b/frontend/src/app/components/settings/settings.component.ts
index 5c9658e..2383f3b 100644
--- a/frontend/src/app/components/settings/settings.component.ts
+++ b/frontend/src/app/components/settings/settings.component.ts
@@ -44,7 +44,7 @@ export class SettingsComponent implements OnInit {
async ngOnInit(): Promise {
this.model = { ...this.data.config };
- const [err, path] = await to(window.backend.isFFmpegInstalled());
+ const [err, path] = await to(window.backend.main.AppState.IsFFmpegInstalled());
if(path) {
this.isFfmpegAvailable = true;
this._cdr.detectChanges();
@@ -52,10 +52,14 @@ export class SettingsComponent implements OnInit {
}
async changeBaseSaveDir(): Promise {
- const [err, path] = await to(window.backend.AppState.SelectDirectory());
+ const [err, path] = await to(window.backend.main.AppState.SelectDirectory());
console.log(err, path)
}
+ async openUrl(url: string): Promise {
+ await window.backend.main.AppState.OpenUrl(url);
+ }
+
save(): void {
this._dialogRef.close({ config: this.model });
}
diff --git a/frontend/src/app/models/app-state.ts b/frontend/src/app/models/app-state.ts
index cbf712c..d7db0c0 100644
--- a/frontend/src/app/models/app-state.ts
+++ b/frontend/src/app/models/app-state.ts
@@ -22,17 +22,20 @@ export interface AppState {
export interface BackendCallbacks {
- AppState: {
- GetAppConfig: () => Promise;
- SelectDirectory: () => Promise
+ main: {
+ AppState: {
+ GetAppConfig: () => Promise;
+ SelectDirectory: () => Promise
+ IsSupportedUrl: (url: string) => Promise;
+ AddToDownload: (url: string, isFromClipboard: boolean) => Promise;
+ StartDownload: (entry: Entry) => Promise;
+ ReadSettingBoolValue: (name: string) => Promise;
+ ReadSettingValue: (name: string) => Promise;
+ SaveSettingBoolValue: (name: string, val: boolean) => Promise;
+ SaveSettingValue: (name: string, val: string) => Promise;
+ RemoveEntry: (entry: Entry) => Promise;
+ IsFFmpegInstalled: () => Promise;
+ OpenUrl: (url :string) => Promise;
+ }
}
- isSupportedUrl: (url: string) => Promise;
- addToDownload: (url: string, isFromClipboard: boolean) => Promise;
- startDownload: (entry: Entry) => Promise;
- readSettingBoolValue: (name: string) => Promise;
- readSettingValue: (name: string) => Promise;
- saveSettingBoolValue: (name: string, val: boolean) => Promise;
- saveSettingValue: (name: string, val: string) => Promise;
- removeEntry: (entry: Entry) => Promise;
- isFFmpegInstalled: () => Promise;
}
diff --git a/frontend/src/app/pages/home.component.ts b/frontend/src/app/pages/home.component.ts
index 808f4fd..577e9e4 100644
--- a/frontend/src/app/pages/home.component.ts
+++ b/frontend/src/app/pages/home.component.ts
@@ -132,7 +132,7 @@ export class HomeComponent implements OnInit {
for (const [key, value] of Object.entries(config)) {
switch(key) {
case 'BaseSaveDir':
- await window.backend.saveSettingValue(key, value as string);
+ await window.backend.main.AppState.SaveSettingValue(key, value as string);
break;
case 'ClipboardWatch':
@@ -141,15 +141,15 @@ export class HomeComponent implements OnInit {
case 'ConcurrentPlaylistDownloads':
case 'ConvertToMp3':
case 'CleanWebmFiles':
- await window.backend.saveSettingBoolValue(key, value as boolean);
+ await window.backend.main.AppState.SaveSettingBoolValue(key, value as boolean);
break;
case 'MaxParrallelDownloads':
- await window.backend.saveSettingValue(key, `${value}`);
+ await window.backend.main.AppState.SaveSettingValue(key, `${value}`);
break;
case 'Telegram':
- await window.backend.saveSettingValue(key, JSON.stringify(value));
+ await window.backend.main.AppState.SaveSettingValue(key, JSON.stringify(value));
break;
}
}
@@ -159,6 +159,7 @@ export class HomeComponent implements OnInit {
this._snackbar.open("Settings has been saved");
this._cdr.detectChanges();
} catch(e) {
+ console.log(e)
this._snackbar.open("An error occured while saving settings");
}
});
@@ -219,14 +220,14 @@ export class HomeComponent implements OnInit {
return;
}
- const isSupported = await window.backend.isSupportedUrl(url);
+ const isSupported = await window.backend.main.AppState.IsSupportedUrl(url);
if(!isSupported) {
this._snackbar.open('Unsupported url');
return
}
try {
- await window.backend.addToDownload(url, false)
+ await window.backend.main.AppState.AddToDownload(url, false)
this.urlInput.setValue('');
this.pasteInput.nativeElement.blur();
this.pasteWrapper.nativeElement.classList.remove('focused');
@@ -239,7 +240,7 @@ export class HomeComponent implements OnInit {
async startDownload(entry: Entry): Promise {
try {
- await window.backend.startDownload(entry);
+ await window.backend.main.AppState.StartDownload(entry);
this._snackbar.open("Started downloading");
} catch(e) {
this._snackbar.open(e);
@@ -249,7 +250,7 @@ export class HomeComponent implements OnInit {
async remove(entry: Entry, i: number): Promise {
console.log('remove', entry, i)
try {
- await window.backend.removeEntry(entry);
+ await window.backend.main.AppState.RemoveEntry(entry);
this._snackbar.open(`${entry.type} has been removed`);
const idx = this.entries.findIndex(e => {
if(entry.type === 'playlist') {
diff --git a/frontend/src/backend/index.js b/frontend/src/backend/index.js
index 64fc4e7..7d6666a 100755
--- a/frontend/src/backend/index.js
+++ b/frontend/src/backend/index.js
@@ -5,6 +5,15 @@
const backend = {
"main": {
"AppState": {
+ /**
+ * AddToDownload
+ * @param {string} arg1 - Go Type: string
+ * @param {boolean} arg2 - Go Type: bool
+ * @returns {Promise} - Go Type: error
+ */
+ "AddToDownload": (arg1, arg2) => {
+ return window.backend.main.AppState.AddToDownload(arg1, arg2);
+ },
/**
* GetAppConfig
* @returns {Promise} - Go Type: *models.AppConfig
@@ -20,6 +29,71 @@ const backend = {
"GetEntryById": (arg1) => {
return window.backend.main.AppState.GetEntryById(arg1);
},
+ /**
+ * IsFFmpegInstalled
+ * @returns {Promise} - Go Type: string
+ */
+ "IsFFmpegInstalled": () => {
+ return window.backend.main.AppState.IsFFmpegInstalled();
+ },
+ /**
+ * IsSupportedUrl
+ * @param {string} arg1 - Go Type: string
+ * @returns {Promise} - Go Type: bool
+ */
+ "IsSupportedUrl": (arg1) => {
+ return window.backend.main.AppState.IsSupportedUrl(arg1);
+ },
+ /**
+ * OpenUrl
+ * @param {string} arg1 - Go Type: string
+ * @returns {Promise} - Go Type: error
+ */
+ "OpenUrl": (arg1) => {
+ return window.backend.main.AppState.OpenUrl(arg1);
+ },
+ /**
+ * ReadSettingBoolValue
+ * @param {string} arg1 - Go Type: string
+ * @returns {Promise} - Go Type: bool
+ */
+ "ReadSettingBoolValue": (arg1) => {
+ return window.backend.main.AppState.ReadSettingBoolValue(arg1);
+ },
+ /**
+ * ReadSettingValue
+ * @param {string} arg1 - Go Type: string
+ * @returns {Promise} - Go Type: string
+ */
+ "ReadSettingValue": (arg1) => {
+ return window.backend.main.AppState.ReadSettingValue(arg1);
+ },
+ /**
+ * RemoveEntry
+ * @param {any} arg1 - Go Type: map[string]interface {}
+ * @returns {Promise} - Go Type: error
+ */
+ "RemoveEntry": (arg1) => {
+ return window.backend.main.AppState.RemoveEntry(arg1);
+ },
+ /**
+ * SaveSettingBoolValue
+ * @param {string} arg1 - Go Type: string
+ * @param {boolean} arg2 - Go Type: bool
+ * @returns {Promise} - Go Type: error
+ */
+ "SaveSettingBoolValue": (arg1, arg2) => {
+ return window.backend.main.AppState.SaveSettingBoolValue(arg1, arg2);
+ },
+ /**
+ * SaveSettingValue
+ * @param {string} arg1 - Go Type: string
+ * @param {string} arg2 - Go Type: string
+ * @returns {Promise} - Go Type: error
+ */
+ "SaveSettingValue": (arg1, arg2) => {
+ return window.backend.main.AppState.SaveSettingValue(arg1, arg2);
+ },
/**
* SelectDirectory
* @returns {Promise} - Go Type: string
@@ -27,6 +101,14 @@ const backend = {
"SelectDirectory": () => {
return window.backend.main.AppState.SelectDirectory();
},
+ /**
+ * StartDownload
+ * @param {any} arg1 - Go Type: map[string]interface {}
+ * @returns {Promise} - Go Type: error
+ */
+ "StartDownload": (arg1) => {
+ return window.backend.main.AppState.StartDownload(arg1);
+ },
}
}
diff --git a/main.go b/main.go
index 4b542a6..4f6dc62 100644
--- a/main.go
+++ b/main.go
@@ -3,58 +3,34 @@ package main
import (
"context"
"embed"
- "errors"
"fmt"
"log"
"net/http"
"os"
- "os/exec"
"os/signal"
"os/user"
"sync"
"time"
. "ytd/clipboard"
- . "ytd/constants"
- . "ytd/db"
. "ytd/models"
. "ytd/plugins"
_ "embed"
- "github.com/mitchellh/mapstructure"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
- "github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/wailsapp/wails/v2/pkg/options/mac"
- "github.com/xujiajun/nutsdb"
- tb "gopkg.in/tucnak/telebot.v2"
)
-var wailsRuntime *wails.Runtime
var plugins []Plugin = []Plugin{&Yt{Name: "youtube"}}
//go:embed frontend/dist/assets/*
var static embed.FS
-//go:embed frontend/dist/main.js
-var js string
-
-//go:embed frontend/dist/styles.css
-var css string
-
var appState *AppState
var newEntries = make(chan GenericEntry)
-type AppState struct {
- runtime *wails.Runtime
- db *nutsdb.DB
- plugins []Plugin
- Entries []GenericEntry `json:"entries"`
- Config *AppConfig `json:"config"`
- Stats *AppStats
-}
-
func cors(fs http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// do your cors stuff
@@ -64,412 +40,8 @@ func cors(fs http.Handler) http.HandlerFunc {
}
}
-func (state *AppState) WailsInit(runtime *wails.Runtime) {
- // Save runtime
- state.runtime = runtime
- // Do some other initialisation
- state.db = InitializeDb()
- state.Entries = DbGetAllEntries()
- state.Config = state.Config.Init()
- state.Stats = &AppStats{}
- appState = state
-
- // this is sync so it blocks until finished and wails:loaded are not dispatched until this finishes
- if runtime.System.AppType() == "default" { // wails serve & ng serve
- runtime.Events.On("wails:loaded", func(...interface{}) {
- time.Sleep(100 * time.Millisecond)
- fmt.Println("EMIT YTD:ONLOAD")
- runtime.Events.Emit("ytd:onload", state)
- })
- } else { // dekstop build
- go func() {
- runtime.Events.Emit("ytd:onload", state)
- }()
- }
-
- for _, plugin := range plugins {
- plugin.SetWailsRuntime(runtime)
- plugin.SetAppConfig(state.Config)
- plugin.SetAppStats(state.Stats)
- }
- fmt.Println("APP STATE INITIALIZED")
-
- /* go func() {
- for {
- time.Sleep(10 * time.Second)
- state.checkForTracksToDownload()
- }
- }() */
-
- /* go func() {
- for {
- restart := make(chan int)
- time.Sleep(3 * time.Second)
- go state.telegramShareTracks(restart)
-
- for {
- select {
- case <-restart:
- fmt.Println("Share tracks...")
- go state.telegramShareTracks(restart)
- }
-
- }
- }
- }() */
-
- go func() {
- restart := make(chan int)
- time.Sleep(3 * time.Second)
- go state.convertToMp3(restart)
-
- for {
- select {
- case <-restart:
- fmt.Println("Restart converting...")
- go state.convertToMp3(restart)
- }
-
- }
- }()
-}
-
-func (state *AppState) GetAppConfig() *AppConfig {
- return state.Config
-}
-
-func (state *AppState) SelectDirectory() (string, error) {
- selectedDirectory, err := state.runtime.Dialog.OpenDirectory(&dialog.OpenDialog{
- AllowFiles: false,
- CanCreateDirectories: true,
- AllowDirectories: true,
- Title: "Choose directory",
- })
- return selectedDirectory, err
-}
-
-func (state *AppState) GetEntryById(entry GenericEntry) *GenericEntry {
- for _, t := range state.Entries {
- if t.Type == "track" && t.Track.ID == entry.Track.ID {
- return &t
- }
- }
- return nil
-}
-
-func (state *AppState) checkForTracksToDownload() error {
- fmt.Printf("Check for tracks to start downloads...%d/%d\n", state.Stats.DownloadingCount, state.Config.MaxParrallelDownloads)
- if state.Stats.DownloadingCount >= state.Config.MaxParrallelDownloads {
- return nil
- }
-
- fmt.Printf("Checking...%d entries\n", len(state.Entries))
- // range over DbGetAllEntries()
- for _, t := range DbGetAllEntries() {
- var freeSlots uint = state.Config.MaxParrallelDownloads - state.Stats.DownloadingCount
- entry := t
- // auto download only tracks with processing status
- // if track has pending/failed status it means that something goes wrong so user have to download it manually from UI
- if entry.Type == "track" && entry.Track.Status == TrackStatusProcessing {
- if freeSlots == 0 {
- return nil
- }
-
- // start download for track
- fmt.Printf("Found %s to download\n", entry.Track.Name)
- plugin := getPluginFor(entry.Source)
- // make chan GenericEntry
- // goriutine scrive li dentro
- if plugin != nil {
- freeSlots--
- go func(entry GenericEntry) {
- // storedEntry := state.GetEntryById(entry)
- // fmt.Printf("Stored entry %v\n", storedEntry)
- // fmt.Println(entry.Track.Url)
- // fmt.Println(storedEntry.Track.Url)
- plugin.StartDownload(&entry)
- // storedEntry.Track = entry.Track
- }(entry)
- }
- }
- }
-
- // qui un for che legge dal channel e ogni volta che riceve una entry downloadata
- return nil
-}
-
-type recipientString string
-
-func (r recipientString) Recipient() string {
- return r.Recipient()
-}
-
-func (state *AppState) telegramShareTracks(restart chan<- int) error {
- if !state.Config.Telegram.Share {
- // if option is not enabled restart check after 30s
- time.Sleep(30 * time.Second)
- restart <- 1
- return nil
- }
-
- b, err := tb.NewBot(tb.Settings{
- Token: "1903196088:AAHhWGvfhQfS_MlhvohFvQYnrg3z7GsBPOM",
- Poller: &tb.LongPoller{Timeout: 10 * time.Second},
- })
-
- if err != nil {
- fmt.Println(err)
- return nil
- }
-
- audio := &tb.Audio{File: tb.FromDisk("/Users/oskarmarciniak/songs/youtube/2vOU4nVI_DA.mp3"), FileName: "Track name", Title: "Track name title", Duration: 207, Caption: "Caption tracks"}
- b.Send(&tb.Chat{ID: 903612486}, audio)
-
- b.Handle("/hello", func(m *tb.Message) {
- fmt.Println(m.Chat)
- b.Send(m.Sender, "Hello World!")
- // cercare il primo msg che come username ha quello impostato nell'app e salvarsi la chat_id
- })
-
- b.Start()
-
- return nil
-}
-
-func (state *AppState) convertToMp3(restart chan<- int) error {
- if !state.Config.ConvertToMp3 {
- // if option is not enabled restart check after 30s
- time.Sleep(30 * time.Second)
- restart <- 1
- return nil
- }
-
- fmt.Println("Converting....")
- ffmpeg, _ := isFFmpegInstalled()
-
- for _, t := range DbGetAllEntries() {
- entry := t
- plugin := getPluginFor(entry.Source)
-
- if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") {
- fmt.Printf("Extracting audio for %s...\n", entry.Track.Name)
-
- // ffmpeg -i "41qC3w3UUkU.webm" -vn -ab 128k -ar 44100 -y "41qC3w3UUkU.mp3"
- cmd := exec.Command(
- ffmpeg,
- "-loglevel", "quiet",
- "-i", fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID),
- "-vn",
- "-ab", "128k",
- "-ar", "44100",
- "-y", fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID),
- )
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
-
- if err := cmd.Run(); err != nil {
- fmt.Println("Failed to extract audio:", err)
- } else {
- entry.Track.IsConvertedToMp3 = true
- DbWriteEntry(entry.Track.ID, entry)
- state.runtime.Events.Emit("ytd:track", entry) // track:converted:mp3
-
- // remove webm
- if state.Config.CleanWebmFiles && plugin.IsTrackFileExists(entry.Track, "webm") {
- err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
- if err != nil && !os.IsNotExist(err) {
- fmt.Printf("Cannot remove %s.webm file after successfull converting to mp3\n", entry.Track.ID)
- }
- }
- }
- restart <- 1
- return nil
- }
- }
-
- // if there are no tracks to convert delay between restart
- time.Sleep(30 * time.Second)
- restart <- 1
- return nil
-}
-
-func saveSettingBoolValue(name string, val bool) (err error) {
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Recovering from panic saveSettingValue:", r)
- switch x := r.(type) {
- case string:
- err = errors.New(x)
- case error:
- err = x
- default:
- // Fallback err (per specs, error strings should be lowercase w/o punctuation
- err = errors.New("unknown panic")
- }
- }
- }()
-
- error := DbSaveSettingBoolValue(name, val)
- if err != nil {
- return error
- }
-
- appState.Config.Set(name, val)
- return nil
-}
-
-func saveSettingValue(name string, val string) (err error) {
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Recovering from panic saveSettingValue:", r)
- switch x := r.(type) {
- case string:
- err = errors.New(x)
- case error:
- err = x
- default:
- // Fallback err (per specs, error strings should be lowercase w/o punctuation
- err = errors.New("unknown panic")
- }
- }
- }()
-
- error := DbWriteSetting(name, val)
- if err != nil {
- return error
- }
-
- appState.Config.Set(name, val)
- return nil
-}
-
-func readSettingBoolValue(name string) (bool, error) {
- return DbReadSettingBoolValue(name)
-}
-
-func readSettingValue(name string) (string, error) {
- return DbReadSetting(name)
-}
-
-func removeEntry(record map[string]interface{}) error {
- var err error
- var entry GenericEntry
- err = mapstructure.Decode(record, &entry)
- if err != nil {
- return err
- }
-
- if entry.Type == "track" {
- if err = DbDeleteEntry(entry.Track.ID); err == nil {
- plugin := getPluginFor(entry.Source)
- if plugin.IsTrackFileExists(entry.Track, "webm") {
- err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
- fmt.Println(err)
- if err != nil && !os.IsNotExist(err) {
- return err
- }
- }
- // remove mp3 if file has been already converted
- if plugin.IsTrackFileExists(entry.Track, "mp3") {
- err = os.Remove(fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID))
- fmt.Println(err)
- if err != nil && !os.IsNotExist(err) {
- return err
- }
- }
- }
- }
- return err
-}
-
-func addToDownload(url string, isFromClipboard bool) error {
- for _, plugin := range plugins {
- if support := plugin.Supports(url); support {
- if appState.GetAppConfig().ConcurrentDownloads {
- go func() {
- plugin.Fetch(url, isFromClipboard)
- }()
- } else {
- plugin.Fetch(url, isFromClipboard)
- }
- continue
- }
- }
- return nil
-}
-
-func startDownload(record map[string]interface{}) error {
- var err error
- var entry GenericEntry
- err = mapstructure.Decode(record, &entry)
- if err != nil {
- return err
- }
-
- if appState.Stats.DownloadingCount >= appState.Config.MaxParrallelDownloads {
- return errors.New("Max simultaneous downloads are reached please retry after some track finished downloading")
- }
-
- for _, plugin := range plugins {
- if plugin.GetName() == entry.Source && entry.Type == "track" {
- if appState.GetAppConfig().ConcurrentDownloads {
- go func() {
- plugin.StartDownload(&entry)
- }()
- } else {
- plugin.StartDownload(&entry)
- }
- continue
- }
- }
- return nil
-}
-
-func isSupportedUrl(url string) bool {
- for _, plugin := range plugins {
- if support := plugin.Supports(url); support {
- return true
- }
- }
- return false
-}
-
-func isFFmpegInstalled() (string, error) {
- ffmpeg, err := exec.LookPath("ffmpeg")
- return ffmpeg, err
-}
-
-func getPluginFor(name string) Plugin {
- for _, plugin := range plugins {
- if plugin.GetName() == name {
- return plugin
- }
- }
- return nil
-}
-
-//WailsRuntime .
-type WailsRuntime struct {
- runtime *wails.Runtime
-}
-
-func (s *WailsRuntime) WailsShutdown() {
- CloseDb()
-}
-
func main() {
- /* app.Bind(&AppState{})
- app.Bind(saveSettingBoolValue)
- app.Bind(saveSettingValue)
- app.Bind(readSettingBoolValue)
- app.Bind(readSettingValue)
- app.Bind(removeEntry)
- app.Bind(addToDownload)
- app.Bind(startDownload)
- app.Bind(isSupportedUrl)
- app.Bind(isFFmpegInstalled) */
-
+ app := &AppState{}
wg := &sync.WaitGroup{}
wg.Add(2)
ctx, cancelCtx := context.WithCancel(context.Background())
@@ -513,7 +85,7 @@ func main() {
if ok && change != "" {
log.Printf("change received: '%s'", change)
if appState.Config.ClipboardWatch {
- addToDownload(change, true)
+ app.AddToDownload(change, true)
}
} else {
log.Println("channel has been closed. exiting...")
@@ -525,12 +97,11 @@ func main() {
go func() {
fs := http.StripPrefix("/static/", http.FileServer(http.FS(static)))
- http.Handle("/tracks/", http.FileServer(http.Dir(currentDir)))
+ http.Handle("/tracks/", http.StripPrefix("/tracks/", http.FileServer(http.Dir(currentDir))))
http.Handle("/static/", cors(fs))
http.ListenAndServe(":8080", nil)
}()
- app := &AppState{}
err := wails.Run(&options.App{
Width: 1024,
Height: 768,