diff --git a/.gitignore b/.gitignore index f749b3a..c175ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.so *.dll gtitles.c +output/ diff --git a/cdecrypt/cdecrypt.c b/cdecrypt/cdecrypt.c index a710b8a..d49c3d5 100644 --- a/cdecrypt/cdecrypt.c +++ b/cdecrypt/cdecrypt.c @@ -346,7 +346,7 @@ out: } #undef BLOCK_SIZE -int cdecrypt_main(int argc, char **argv) { +int cdecrypt_main(int argc, char **argv, int *progress) { int r = EXIT_FAILURE; char str[PATH_MAX], *tmd_path = NULL, *tik_path = NULL; FILE *src = NULL; @@ -526,6 +526,7 @@ int cdecrypt_main(int argc, char **argv) { uint32_t level = 0; for (uint32_t i = 1; i < entries; i++) { + *progress = (i * 100) / entries; if (level > 0) { while ((level >= 1) && (l_entry[level - 1] == i)) level--; diff --git a/cdecrypt/cdecrypt.h b/cdecrypt/cdecrypt.h index 6896f7c..2748a40 100644 --- a/cdecrypt/cdecrypt.h +++ b/cdecrypt/cdecrypt.h @@ -1,3 +1,3 @@ #pragma once -int cdecrypt_main(int argc, char **argv); +int cdecrypt_main(int argc, char **argv, int *progress); diff --git a/certificate.go b/certificate.go index 602c020..af45671 100644 --- a/certificate.go +++ b/certificate.go @@ -26,11 +26,11 @@ func getCert(tmdData []byte, id int, numContents uint16) ([]byte, error) { } } -func getDefaultCert(client *grab.Client) ([]byte, error) { +func getDefaultCert(progressWindow *ProgressWindow, client *grab.Client) ([]byte, error) { if len(cetkData) >= 0x350+0x300 { return cetkData[0x350 : 0x350+0x300], nil } - if err := downloadFile(client, "http://ccs.cdn.c.shop.nintendowifi.net/ccs/download/000500101000400a/cetk", "cetk"); err != nil { + if err := downloadFile(progressWindow, client, "http://ccs.cdn.c.shop.nintendowifi.net/ccs/download/000500101000400a/cetk", "cetk"); err != nil { return nil, err } cetkData, err := os.ReadFile("cetk") diff --git a/cmd/WiiUDownloader/mainwindow.go b/cmd/WiiUDownloader/mainwindow.go index 379ce13..6d4081f 100644 --- a/cmd/WiiUDownloader/mainwindow.go +++ b/cmd/WiiUDownloader/mainwindow.go @@ -9,9 +9,16 @@ import ( "github.com/gotk3/gotk3/gtk" ) +const ( + NAME_COLUMN = 0 + TITLE_ID_COLUMN = 1 + REGION_COLUMN = 2 +) + type MainWindow struct { - window *gtk.Window - titles []wiiudownloader.TitleEntry + window *gtk.Window + treeView *gtk.TreeView + titles []wiiudownloader.TitleEntry } func NewMainWindow(entries []wiiudownloader.TitleEntry) *MainWindow { @@ -43,7 +50,7 @@ func (mw *MainWindow) ShowAll() { for _, entry := range mw.titles { iter := store.Append() err = store.Set(iter, - []int{0, 1, 2}, + []int{NAME_COLUMN, TITLE_ID_COLUMN, REGION_COLUMN}, []interface{}{entry.Name, fmt.Sprintf("%016x", entry.TitleID), wiiudownloader.GetFormattedRegion(entry.Region)}, ) if err != nil { @@ -51,7 +58,7 @@ func (mw *MainWindow) ShowAll() { } } - treeView, err := gtk.TreeViewNewWithModel(store) + mw.treeView, err = gtk.TreeViewNewWithModel(store) if err != nil { log.Fatal("Unable to create tree view:", err) } @@ -60,40 +67,65 @@ func (mw *MainWindow) ShowAll() { if err != nil { log.Fatal("Unable to create cell renderer:", err) } - column, err := gtk.TreeViewColumnNewWithAttribute("Name", renderer, "text", 0) + column, err := gtk.TreeViewColumnNewWithAttribute("Name", renderer, "text", NAME_COLUMN) if err != nil { log.Fatal("Unable to create tree view column:", err) } - treeView.AppendColumn(column) + mw.treeView.AppendColumn(column) renderer, err = gtk.CellRendererTextNew() if err != nil { log.Fatal("Unable to create cell renderer:", err) } - column, err = gtk.TreeViewColumnNewWithAttribute("Title ID", renderer, "text", 1) + column, err = gtk.TreeViewColumnNewWithAttribute("Title ID", renderer, "text", TITLE_ID_COLUMN) if err != nil { log.Fatal("Unable to create tree view column:", err) } - treeView.AppendColumn(column) + mw.treeView.AppendColumn(column) - column, err = gtk.TreeViewColumnNewWithAttribute("Region", renderer, "text", 2) + column, err = gtk.TreeViewColumnNewWithAttribute("Region", renderer, "text", REGION_COLUMN) if err != nil { log.Fatal("Unable to create tree view column:", err) } - treeView.AppendColumn(column) + mw.treeView.AppendColumn(column) + + mw.treeView.Connect("row-activated", mw.onRowActivated) scrollable, err := gtk.ScrolledWindowNew(nil, nil) if err != nil { log.Fatal("Unable to create scrolled window:", err) } scrollable.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrollable.Add(treeView) + scrollable.Add(mw.treeView) mw.window.Add(scrollable) mw.window.ShowAll() } +func (mw *MainWindow) onRowActivated() { + selection, err := mw.treeView.GetSelection() + if err != nil { + log.Fatal("Unable to get selection:", err) + } + + model, iter, _ := selection.GetSelected() + if iter != nil { + tid, _ := model.ToTreeModel().GetValue(iter, TITLE_ID_COLUMN) + if tid != nil { + if tidStr, err := tid.GetString(); err == nil { + fmt.Println("Cell Value:", tidStr) + progressWindow, err := wiiudownloader.CreateProgressWindow(mw.window) + if err != nil { + return + } + progressWindow.Window.ShowAll() + go wiiudownloader.DownloadTitle(tidStr, "output", true, &progressWindow) + } + } + } +} + func Main() { gtk.Main() } diff --git a/decryption.go b/decryption.go index 844dcc8..2133ec3 100644 --- a/decryption.go +++ b/decryption.go @@ -11,15 +11,42 @@ package wiiudownloader import "C" import ( "fmt" + "sync" + "time" "unsafe" ) -func decryptContents(path string) error { - argv := make([]*C.char, 2) - argv[0] = C.CString("WiiUDownloader") - argv[1] = C.CString(path) - if int(C.cdecrypt_main(2, (**C.char)(unsafe.Pointer(&argv[0])))) != 0 { +var ( + wg sync.WaitGroup + decryptionDone = false + decryptionError = false +) + +func decryptContents(path string, progress *int) error { + wg.Add(1) + go runDecryption(path, progress) + for !decryptionDone { + fmt.Println(*progress) + time.Sleep(500 * time.Millisecond) + } + + wg.Wait() + + if decryptionError { + decryptionDone = false + decryptionError = false return fmt.Errorf("decryption failed") } return nil } + +func runDecryption(path string, progress *int) { + defer wg.Done() + argv := make([]*C.char, 2) + argv[0] = C.CString("WiiUDownloader") + argv[1] = C.CString(path) + if int(C.cdecrypt_main(2, (**C.char)(unsafe.Pointer(&argv[0])), (*C.int)(unsafe.Pointer(progress)))) != 0 { + decryptionError = true + } + decryptionDone = true +} diff --git a/downloader.go b/downloader.go index 62708c5..6887a27 100644 --- a/downloader.go +++ b/downloader.go @@ -10,23 +10,95 @@ import ( "strings" "github.com/cavaliergopher/grab/v3" + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" ) -func downloadFile(client *grab.Client, url string, outputPath string) error { +type ProgressWindow struct { + Window *gtk.Window + box *gtk.Box + label *gtk.Label + bar *gtk.ProgressBar + percentLabel *gtk.Label +} + +func CreateProgressWindow(parent *gtk.Window) (ProgressWindow, error) { + win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) + if err != nil { + return ProgressWindow{}, err + } + win.SetTitle("File Download") + + win.SetTransientFor(parent) + + box, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 5) + if err != nil { + return ProgressWindow{}, err + } + win.Add(box) + + filenameLabel, err := gtk.LabelNew("File: ") + if err != nil { + return ProgressWindow{}, err + } + box.PackStart(filenameLabel, false, false, 0) + + progressBar, err := gtk.ProgressBarNew() + if err != nil { + return ProgressWindow{}, err + } + box.PackStart(progressBar, false, false, 0) + + percentLabel, err := gtk.LabelNew("0%") + if err != nil { + return ProgressWindow{}, err + } + box.PackStart(percentLabel, false, false, 0) + + return ProgressWindow{ + Window: win, + box: box, + label: filenameLabel, + bar: progressBar, + percentLabel: percentLabel, + }, nil +} + +func downloadFile(progressWindow *ProgressWindow, client *grab.Client, url string, outputPath string) error { req, err := grab.NewRequest(outputPath, url) if err != nil { return err } + resp := client.Do(req) if err := resp.Err(); err != nil { return err } - fmt.Printf("[Info] Download saved to ./%v \n", resp.Filename) + progressWindow.label.SetText(fmt.Sprintf("File: %s", resp.Filename)) + + go func() { + for !resp.IsComplete() { + glib.IdleAdd(func() { + progress := float64(resp.BytesComplete()) / float64(resp.Size()) + progressWindow.bar.SetFraction(progress) + progressWindow.percentLabel.SetText(fmt.Sprintf("%.0f%%", progress*100)) + }) + } + + progressWindow.bar.SetFraction(1) + + glib.IdleAdd(func() { + progressWindow.Window.SetTitle("Download Complete") + }) + }() + return nil } -func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) error { +func DownloadTitle(titleID string, outputDirectory string, doDecryption bool, progressWindow *ProgressWindow) error { + progress := 0 + currentFile := "" outputDir := strings.TrimRight(outputDirectory, "/\\") baseURL := fmt.Sprintf("http://ccs.cdn.c.shop.nintendowifi.net/ccs/download/%s", titleID) titleKeyBytes, err := hex.DecodeString(titleID) @@ -41,7 +113,7 @@ func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) er client := grab.NewClient() downloadURL := fmt.Sprintf("%s/%s", baseURL, "tmd") tmdPath := filepath.Join(outputDir, "title.tmd") - if err := downloadFile(client, downloadURL, tmdPath); err != nil { + if err := downloadFile(progressWindow, client, downloadURL, tmdPath); err != nil { return err } @@ -57,7 +129,7 @@ func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) er tikPath := filepath.Join(outputDir, "title.tik") downloadURL = fmt.Sprintf("%s/%s", baseURL, "cetk") - if err := downloadFile(client, downloadURL, tikPath); err != nil { + if err := downloadFile(progressWindow, client, downloadURL, tikPath); err != nil { return err } tikData, err := os.ReadFile(tikPath) @@ -85,7 +157,7 @@ func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) er } cert.Write(cert1) - defaultCert, err := getDefaultCert(client) + defaultCert, err := getDefaultCert(progressWindow, client) if err != nil { return err } @@ -108,17 +180,18 @@ func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) er if err := binary.Read(bytes.NewReader(tmdData[offset:offset+4]), binary.BigEndian, &id); err != nil { return err } - - appPath := filepath.Join(outputDir, fmt.Sprintf("%08X.app", id)) + currentFile = fmt.Sprintf("%08X.app", id) + appPath := filepath.Join(outputDir, currentFile) downloadURL = fmt.Sprintf("%s/%08X", baseURL, id) - if err := downloadFile(client, downloadURL, appPath); err != nil { + if err := downloadFile(progressWindow, client, downloadURL, appPath); err != nil { return err } if tmdData[offset+7]&0x2 == 2 { - h3Path := filepath.Join(outputDir, fmt.Sprintf("%08X.h3", id)) + currentFile = fmt.Sprintf("%08X.h3", id) + h3Path := filepath.Join(outputDir, currentFile) downloadURL = fmt.Sprintf("%s/%08X.h3", baseURL, id) - if err := downloadFile(client, downloadURL, h3Path); err != nil { + if err := downloadFile(progressWindow, client, downloadURL, h3Path); err != nil { return err } var content contentInfo @@ -132,8 +205,12 @@ func DownloadTitle(titleID string, outputDirectory string, doDecryption bool) er } if doDecryption { - decryptContents(outputDir) + if err := decryptContents(outputDir, &progress); err != nil { + return err + } } + progressWindow.Window.Close() + return nil }