Repository: ninthwalker/saverr
Branch: master
Commit: 08c2d0ad47d4
Files: 5
Total size: 110.5 KB
Directory structure:
gitextract_tny7vz03/
├── LICENSE
├── README.md
├── Saverr.lnk
├── Saverr.ps1
└── screenshots/
└── README.MD
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 ninthwalker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Saverr
## Developers Note:
This script/repo is no longer maintained.
While it still works as far as i know, I'm not adding/changing to it any longer. I would suggest to check out a project called [PlexRipper](https://github.com/PlexRipper/PlexRipper) if you are interested in something like my Saverr, but with a lot better GUI and feature set. Thanks!
Powershell script with a front-end GUI to download media from Plex Servers.
Allows you to search and save movies, tv shows, and music for offline/personal use.
Expanded from the original non-gui version: [directplexDownloader](https://github.com/ninthwalker/directPlexDownloader).
## Details/Requirements
1. Windows 7/8/10
2. Powershell 3.0+
3. .Net Framework 3.5+ (Usually already on your windows computer)
4. Plex Server (Can be used on your own server or a remote friends server as well)
Supports:
* **Movies, TV Shows, and Music:** Download any and all media. Full TV Series or Albulms as well.
* **Searching:** Enter the name of what you are looking for to watch what you want, when you want.
* **Multiple Server support:** Quickly switch between multiple plex servers.
* **Size/ETA Estimates:** Shows current progress, size of download as well as an estimated time of finishing.
* **Pause/Resume:** Supports pausing and resuming of downloads.
## Launching Saverr
**Note:**
Depending on your download method/settings, you may need to 'unblock' the files after downloading them from Github. This is normal behavior for Microsoft Windows to do for files downloaded from the internet.
`Right click > Properties > Check 'Unblock'`
**Option One**
1. Copy the Saverr.lnk shortcut, as well as the Saverr.ps1 file to your computer. Place in the same directory.
2. Double click the Saverr shortcut to launch the app.
3. Click the gear icon to configure initial settings. (See settings section below)
**Option Two**
1. Copy the Saverr.ps1 file to your desktop.
2. Open a powershell console (Not ISE) and navigate to the folder you saved the Saverr.ps1 file.
3. Enter the below command to temporarily set the execution policy:
`Set-ExecutionPolicy -Scope Process Bypass`
Alternatively, set the execution policy to permanently allow powershell scripts:
`Set-ExecutionPolicy -Scope Currentuser Unrestricted`
4. Then enter the below command to launch the app:
`.\Saverr.ps1`
**Option Three**
Not available anymore. Even though the .exe was just the powershell script compiled, windows would flag it as a virus so I removed this option. You can compile it yourself into an executable using this tool here if you would like:
(https://gallery.technet.microsoft.com/scriptcenter/PS2EXE-GUI-Convert-e7cb69d5)
~~If you trust me, I've compiled an .exe here as well.~~
~~source is just the Saverr.ps1 file that is located here converted with the [PS2EXE] tool. You can do it yourself if you want using the Saverr.ps1 file.~~
~~Save the Saverr.exe to your computer and double click it to launch the app.~~
## How to use
1. Launch the script using one of the methods listed above.
2. Enter the name of the Movie, TV show or Music artist to search for.
3. Select the desired result from the results box.
3a. If a Movie: Just click download.
3b. If a TV Show, select the season or episodes, then click download. (Can also select All seasons or All episodes)
3c. If Music, select the album or tack, then click download. (Can also select All albums or All tracks)
## Config/Settings
All config is done in the settings menu (Accessed by clicking the orange gear icon)
* **Plex Username:** Plex Username. Can be an email or a username. (this is not saved)
* **Plex Password:** Your Plex password. (this is not saved, only used to retrieve your plex token)
Note for 2FA: If 2FA is enabled, please add your 2FA token to the end of your password. ie: MyP@ssword347821
* **Server:** After a token has been saved from entering a username/password you can select a Plex server to search from.
* **Download Path:** Navigate and select a path to save downloads to.
* **SSL Required:** Select this if the server you are trying to connect to has 'Secure Connections: Required' enabled.
* **Debug:** Check to enable a log file for debugging issues. (Saves to saverrLog.txt)
## Known Issues
1. Some plex servers will not work or may act a bit funky.
Not much I can do about this. Everyones plex servers are set up a bit differently. Some have firewalls, special routing configuration, or nginx proxy servers in front. However, from my experience using mutiple plex servers, this works the majority of the time and the servers that do not work are outliers.
3. No mimimize button during an active download.
This is intended. Because of the powershell script functions used in the background, a pause is required to prevent hanging of the script. The mimimize button becomes available after you pause a download. Click resume after maximizing the window again. You can also continue to do other things on your computer by opening other windows while the Saverr app downloads. Saverr will remain open behind these windows and can be re-selected again to use it without pausing of downloads.
4. Maximum number of downloads is 200.
This is the default setting within windows for the BITS download function being used.
More than 200 items will automatically be truncated to 200.
If you need to increase the maximum download amount, please set this registry setting below to the value desired:
**Path:** `HKLM\Software\Policies\Microsoft\Windows\BITS`
**Dword:** `MaxFilesPerJob`
**Decimal Value:** `Dealers Choice`
Reference: [Bits](https://docs.microsoft.com/en-us/windows/desktop/bits/group-policies)
## Errors
Some errors are self explanatory and output to the main app window, others are not. You can enable debugging in the settings menu. This will create a log file (saverrLog.txt) in the current Saverr directory that will give more information on the error or issue.
## Screenshots





================================================
FILE: Saverr.ps1
================================================
#######################################
# Name: Saverr #
# Desc: d/l media from Plex #
# Author: Ninthwalker #
# Date: 16NOV2021 #
# Version: 1.1.2 #
#######################################
###### NOTES FOR USER #######
# See Instruction online at: https://github.com/ninthwalker/saverr
# Execution policy may need to be set to run powershell scripts if not using the shortcut from Github
# ie: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
# Enforce TLS 1.1/1.2 if wanting. Uses HTTPS/SSL by default to retrieve plex tokens. May/may not break functionality depending on network setup.
#[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls
# If you need to increase the maximum downloads, please set this registry setting below to value desired.
# Bitstransfers default limit is 200. see: https://docs.microsoft.com/en-us/windows/desktop/bits/group-policies
# Path: HKLM\Software\Policies\Microsoft\Windows\BITS
# Dword: MaxFilesPerJob
# Decimal Value: Dealers Choice
#############################
####### DO NOT MODIFY #######
#############################
### setup environment ###
if ($MyInvocation.MyCommand.CommandType -eq "ExternalScript") {
$ScriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
}
else {
$ScriptPath = Split-Path -Parent -Path ([Environment]::GetCommandLineArgs()[0])
if (!$ScriptPath) {
$ScriptPath = "."
}
}
# Set-Location $PSScriptRoot. Changed to above so it works with an .exe as well.
Set-Location $ScriptPath
#import Bitstransfer if not
if (!(Get-Module BitsTransfer)) {
Import-Module BitsTransfer
}
# Set timeout value for how long to wait for download to start before giving up
$timeout = 30
# Get maximum BITS files value if set
$bitsRegistry = 'HKLM:\Software\Policies\Microsoft\Windows\BITS'
$key = Get-Item -LiteralPath $bitsRegistry -ErrorAction SilentlyContinue
if ($key) {
$limit = $key.GetValue("MaxFilesPerJob", 200)
}
# diff ways of d/l. invoke-restmethod seems to fail sometimes while webclient method does not.
# Set-Alias -Name plx -Value Invoke-RestMethod -Scope Script
# download function shortcut
function plx {
Param([Parameter(Mandatory=$true)]
[string]$url
)
# Will timeout after 20sec by default
[xml](New-Object System.Net.WebClient).DownloadString($url)
}
# check invalid char's function
Function Remove-InvalidChars {
Param([Parameter(Mandatory=$true)]
[string]$name
)
$strip = $name -replace('[][]','')
$invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
$re = "[{0}]" -f [RegEx]::Escape($invalidChars)
return ($strip -replace $re)
}
# download path function
Function Get-SavePath($initialDirectory) {
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
$foldername = New-Object System.Windows.Forms.FolderBrowserDialog
$foldername.Description = "Select a folder to save downloads to"
$foldername.rootfolder = "MyComputer"
if($foldername.ShowDialog() -eq "OK")
{
$folder += $foldername.SelectedPath
}
else {
$folder = $false
}
return $folder
}
# logging function
function logIt {
if ($debug) {
$e = $_.Exception
$line = $_.InvocationInfo.ScriptLineNumber
$msg = $e.Message
$eMSG = "$(Get-Date): caught exception: $e at $line. $msg"
$eMSG | Out-File ".\saverrLog.txt" -Append
}
}
# display size function
function byteSize($num)
{
$suffix = "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"
$index = 0
while ($num -gt 1kb)
{
$num = $num / 1kb
$index++
}
"{0:N1} {1}" -f $num, $suffix[$index]
}
# Import settings
if (Test-Path .\saverrSettings.xml) {
$script:settings = Import-Clixml .\saverrSettings.xml
if ((!($settings.name)) -or (!($settings.server)) -or (!($settings.userToken)) -or (!($settings.serverToken)) -or (!($settings.dlPath))) {
$errorMsg = "Settings are not fully configured.`nPlease click the gear icon before searching."
}
else {
$errorMsg = ""
}
}
else {
$errorMsg = "Settings file not detected. Please configure settings before searching."
}
# enable/disable debug
$debug = $settings.logging
# enable/disable ssl
$ssl = $settings.ssl
# The below is needed when the plex server has 'Secure connections: required' set.
# when SSL is enforced, and the 'SSL Required' is checked on the Saverr settings page we will use HTTPS.
# However, because we have to access by IP, the cert will show as 'invalid' since the CN will not match the IP.
# These settings here, allow us to download from servers that enforce the SSL.
# also, later on we do a similar 'ignore cert errors' for the bitstransfer job
# using the below command/info:
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/bitsadmin-setsecurityflags
# https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc753211(v=ws.10)?redirectedfrom=MSDN
# bitsadmin /SetSecurityFlags myJob 30
if ($ssl -eq $True) {
$scheme = "https://"
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
}
else {
$scheme = "http://"
}
# Plex signin for token url
$plexSignInUrl = "https://plex.tv/users/sign_in.xml"
# Plex servers list URL
$plexServersUrl = "https://plex.tv/pms/servers"
# init the cancel/pauseLoop variables
$script:cancelLoop = $false
$script:pauseLoop = $false
### Load required libraries ###
Add-Type -AssemblyName System.Windows.Forms, PresentationFramework, PresentationCore, WindowsBase, System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
################ Images ##################
# load these images inside the script so external calls arents
$plexImg = @'
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
'@
$loadingImg = @'
iVBORw0KGgoAAAANSUhEUgAAAHEAAACmCAYAAADtRWBHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xMkM
Ea+wAABC7SURBVHhe7Z0LlFTFncZ5DDCDAiIg4mOAYYZ5dE8/pqe7ZwbQAA4rIIxCQIYguCoqKL5FEZEEBBQTWRE0AopgwPg6RvNAjTGaXTdns48TyZo1iWvM6mbduBqjJhLpqt7v33
RP/tVTPV0TWovb1HfO79z57nTfrqqv696qupehRzKZdHgc7U6Ht9DudHgL7U6Ht9DudHgL7U6Ht9DudHgL7U6Ht9DudHgL7U6Ht9DudHgL7U6Ht9DuzIeTfSl5cGOKk30peXBjipN9K
XlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9
KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN9KXlwY4qTfSl5cGOKk30peXBjipN
9KXlwY4qTfSl5cGOKk30peXBjilNn+Xy+njU1NQOwDfv9/i/X1dW1V1VVDUr/uuBS8uDGlKNdCKoXGAhGgji4EfyD3+f7IBjwi5ZonQwF/BJh7sf+cQg3/c7CScmDG1OORiGQEvSu4R
QKmA9/J7b7sf2YgpsQr5VfmjpGLm8/VT547dDEhFitwO/+jNdsQIh90ocpmJQ8uDHlaFF9fX0fhFADFiGQ1djuwvY18El9vV9ScEtnjRR3XnKieOym48V/bC0TH+3tLV+6bWAiHPQl8
TrqjTtra2uPTx+yYFLy4MaUYhYavRfCG4ntxWA3eBn8Dnxaj1Ci4TqxYGpFYvtVw8SL6wfKN7aVyg/3lMhPHuktDzzSO0nbC9tGCRwjCSjEp9GDR6UPXzApeXBjSrEpHA73RmNXobGv
x/af0fhvYPt7cBBICmTmxCqx4+phif2b+4u37+8nP9xbQqHJPz/aO8l5c3s/EYvUZkKk3vgKenBL+qMKJiUPbkzxqtCgdIqkbU8E1heNGwPbwG+w71P8LhVYNqGAT77+9VKRHVg2FOr
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
gpae6E4FPbBU7Oie+iuKEOk+Id3QPRmMKGJOAUuy6k4UR4hgdiAQKOo78giwF5iXVXfCkyF+LasSLsQCSMmDG1O6IxeiUnfCkyF+NasSLsQCSMmDG1O6IxeiUnfChegVuRCLQC7EIpA
LsQjkQiwCFVWIqIgLUcWTId6RVQkXYgGk5MGNKd2RC1GpO+FC9IpciEWgYgtxY1YlXIgFkJIHN6Z0Ry5Epe6EC9ErciEWgVyIRaBiC/H2rEq4EAsgJQ9uTOmOXIhK3YniCTEUCvWaM2
dOj2KFvqRFE6LP57stqxL03Ok6bKeDaUUM/dHe7C+wZ/+59wpNRegP2n6In4sWqh9Q/iwaoKffp6eb5rCk5MGNKd0RQqQ/BJv3j9geDaAdXgdD001zWFLy4MaU7giFp2vDFwD9Pxcm/
0FJ0YF6f4rLyvPY+ugPNBVCSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7c
mOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7
cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cmOJkX0oe3JjiZF9KHtyY4mRfSh7cOLyJdqfDW2h3OryFdqfDW2h3OryFdqfDW2h3OryFdqfDW2h3OryFdq
fDW2h3OryFdqfDSyR7/D/88mPYQlLgrwAAAABJRU5ErkJggg==
'@
$iconImg = @'
AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAMMOAADDDgAAAAAAAAAAAAAtLS0ZLS0tyS0tLbQtLS2gLS0toS0tLaEtLS2hLS0toS0tLaEtLS2hLS0toS0
tLaEtLS2hLS0toC0tLbItLS3qLS0tIS0tLcgtLS0yLCwsAi0tLQUtLS0FLS0tBS0tLQUtLS0FLS0tBS0tLQUtLS0FLS0tBSwsLAItLS0xLS0t6C0tLSEtLS3ILS0tLi0tLQAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtLS0ALS0tLS0tLegtLS0hLS0tyC0tLS4tLS0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALS0tAC0tLS0tLS3oLS0tI
S0tLcgtLS0uLS0tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC0tLQAtLS0tLS0t6C0tLSEtLS3JLiopNzQVBggxIRoLLS0sCyMmKwsYHyoLGyEqCykrLAsuKigLMh0T
CzMbEAswIhsILS0tNi0tLektLSwgLS4u1yk5QdAoPUfCKjc9wy0sK8M2My7DQDkvwz03L8MuLi3DKy8ywyg8RsMoPEbDKzM3wi0sK84tLS36LSwsIC0tLtwfWHT/C5LS/w2Myf8iS1//RDw
w/5NtNf+oeTb/b1cy/zA1Nv8XbZX/CZfa/xGAtP8oPEb/LSws/y0tLSEtLCvcLC8w/xpni/QLk9TzEn6w9ig8RfdRRC/3mnE1951zNfdbSjH3KTlA9xR5qPYLktL2GWuS9ysyNfAuKylRKD
tD7yVFVPssMTTEKjY7eydBTk0pNz4/KSosQD04L0JHPjBCQzwwQi8rKUIrMzdCJ0JOQiZDUUMrMzY4LSsrzic+Sf8MkdD/EIO5/ic/S+wuKyq5LS0tbR8kLCYKFCgDERsqAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAC0tLFgsLzCkGWiN4wqW2P0aZor/MC4s/2RQMv5dSzHmOjUuqSwsLVovKCUbOwMAAjYRBQAAAAAAAAAAAAAAAAAvJyMAMCQeBC4pJyYkSVpqIk1htystL+1sVDL/
uYM2/4ZlNP8xLy37KTg+2Co2PJEvJiJCLiglDk0AAAAsLCwAAAAAAAAAAACEAAAAB0lIADUgHwkqKyw0PzkvfnhcM8iPajX1Qzot/yBRaP8QhLv/HV188ysyNcMtLCtALS0tAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAEhAAAAAAAVHSwQQDowRDYxLJAoO0TUDorF+Aee5v8jTGD+LikmYS0tLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC0tLQAsLS4CLiooHiVFVGMZaI
y9IFNq5C4pJjQtLS0AAAAAAAAAAAAf/AAAH/wAAB/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAADwAAgAMAAPABAAD8AQAA/4EAAA==
'@
$settingsImg = @'
iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xMkMEa+w
AAALtSURBVEhLtVVPSBRRGJ/V3ZnR3XVllS1rzf4RIWWWVFJ06FZ46Bh08ZS3oGMk1bW6RIHXwpz3ZmZ3dS3QEgLzVJQdoqAo8iIlRaQEIevO+17fPF+5jU93gvrBjx3e9/t+385733xPWw
/lIb2HUd1ixHy14Han5bIAjJ8wGDVfMFsfgpHNJ+VyeECh6TgjsSlOIpwTTZDRukE+3ReTEs1z4qO/Yj6B6pMlq75ThqsDbH0X0NqlSpNlo9hbZsXGGI1+CMZEPJ/pkBbhwJzkrMpoTTpxL
lPDAcYPZRmp/aE0W5MRXnKze6XFapSKbZ18ukvsN4zvNDyiP1IbrU88x0mea9sofN6dMxaLbVv9ZwHsoCnc7xlmRR/g75zKICzRa55R4wnQ6BwWvScKlGmyp7KL/jXBSRzFtzAsVTBIZpu3
IN9yDGyzFfIbDjOn4TqQyKoODJKRuqsas+ufqYKV9OxUn3jtADy3+XS1QtjWgxpMdMSZnRxQCXwyYoxKTyXKxHBVeZzW8LLddErKlsFI9KVKDG7qrJQowXOZPlUejpwBKVkBnk1BJfbcpjN
SooSX392rygOqnZeSFWBl5ajgtn5BSpTwiHlZlYdD9envWccnexs9J/nHsKskHuwsFHe0CnEAy5M4+l6V5xNjFp55Bs/CrNpd2L7PFwtbtktvARg+0IIf3mOVvpLMTb/xt+m2Khgks2q+YX
NQRiI3gNTeBVLzRaUL0rPMK9qSk+z+r198ftMR8er+HQFOfAbwYgJqfFaJwxLsOumjf8JvbEgU8MGLqa0f73fV+88LD9vTuC0jKoNqxIF4k+faE77PfHFfIww3t/jPSoCbTeM0/q4yWovYB
F95ThMFQuNvb0ZWyLyWqeEAww0HmRUprTKi5phnm/3gmBPBGN4fSziCtkmL6ig52T3MSTiVXefZDf0yLIBD9VplEUb0O4t2auUmDAueS3XhP7/EaGICD1SXywL+yPDnHd4vF0s0tV8uK6Bp
PwEAHUcTE+ClVQAAAABJRU5ErkJggg==
'@
# Image logo function
function DecodeBase64Image {
param ([Parameter(Mandatory=$true)][String]$ImageBase64)
$ObjBitmapImage = New-Object System.Windows.Media.Imaging.BitmapImage #Provides a specialized BitmapSource that is optimized for loading images using Extensible Application Markup Language (XAML).
$ObjBitmapImage.BeginInit() #Signals the start of the BitmapImage initialization.
$ObjBitmapImage.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($ImageBase64) #Creates a stream whose backing store is memory.
$ObjBitmapImage.EndInit() #Signals the end of the BitmapImage initialization.
$ObjBitmapImage.Freeze() #Makes the current object unmodifiable and sets its IsFrozen property to true.
$ObjBitmapImage
}
$plexImgDecoded = DecodeBase64Image -ImageBase64 $plexImg
$loadingImgDecoded = DecodeBase64Image -ImageBase64 $loadingImg
$settingsImgDecoded = DecodeBase64Image -ImageBase64 $settingsImg
$loading = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($loadingImgDecoded.StreamSource)
$logo = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($plexImgDecoded.StreamSource)
$gear = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($settingsImgDecoded.StreamSource)
# Icon
$iconBase64 = $iconImg
$iconBytes = [Convert]::FromBase64String($IconBase64)
$stream = New-Object IO.MemoryStream($iconBytes, 0, $iconBytes.Length)
$stream.Write($iconBytes, 0, $iconBytes.Length);
#################### FORMS ##############################
#Defaults
$label_mediaTitle_default_xy = New-Object System.Drawing.Point(140,255)
$label_mediaRating_default_xy = New-Object System.Drawing.Point(140,275)
$label_mediaScore_default_xy = New-Object System.Drawing.Point(215,275)
$label_mediaSummary_default_xy = New-Object System.Drawing.Point(140,295)
$label_mediaSummary_default_height = 110
# main form
$form = New-Object system.Windows.Forms.Form
$form.ClientSize = '550,500'
$form.text = "Saverr"
$form.BackColor = "#4a4a4a"
$form.TopMost = $false
$form.StartPosition = 'CenterScreen'
$form.Icon = [System.Drawing.Icon]::FromHandle((New-Object System.Drawing.Bitmap -Argument $stream).GetHIcon())
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$label_title = New-Object system.Windows.Forms.Label
$label_title.text = "Saverr"
$label_title.AutoSize = $true
$label_title.width = 30
$label_title.height = 20
$label_title.location = New-Object System.Drawing.Point(143,12)
$label_title.Font = 'Microsoft Sans Serif,15,style=Bold'
$label_title.ForeColor = "#f5a623"
$pictureBox_logo = New-Object system.Windows.Forms.PictureBox
$pictureBox_logo.width = 75
$pictureBox_logo.height = 75
$pictureBox_logo.location = New-Object System.Drawing.Point(15,15)
$pictureBox_logo.image = $logo
$pictureBox_logo.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::normal
$pictureBox_thumb = new-object Windows.Forms.PictureBox
$pictureBox_thumb.ImageLocation = ""
$pictureBox_thumb.Visible = $false
$pictureBox_thumb.Width = 113 #680
$pictureBox_thumb.Height = 166 #1000
$pictureBox_thumb.Anchor = [System.Windows.Forms.AnchorStyles]::Top
$pictureBox_thumb.Location = New-object System.Drawing.Size(15,250)
$pictureBox_thumb.image = $loading
$pictureBox_thumb.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
$label_search = New-Object system.Windows.Forms.Label
$label_search.text = "Search Movie:"
$label_search.AutoSize = $true
$label_search.width = 30
$label_search.height = 15
$label_search.location = New-Object System.Drawing.Point(15,105)
$label_search.Font = 'Microsoft Sans Serif,8'
$label_search.ForeColor = "#f5a623"
$textBox_search = New-Object system.Windows.Forms.TextBox
$textBox_search.multiline = $false
$textBox_search.text = ""
$textBox_search.width = 300
$textBox_search.height = 20
$textBox_search.location = New-Object System.Drawing.Point(15,125)
$textBox_search.Font = 'Microsoft Sans Serif,10'
$button_search = New-Object system.Windows.Forms.Button
$button_search.BackColor = "#f5a623"
$button_search.text = "Search"
$button_search.width = 80
$button_search.height = 25
$button_search.location = New-Object System.Drawing.Point(345,124)
$button_search.Font = 'Microsoft Sans Serif,9,style=Bold'
$button_search.FlatStyle = "Flat"
$button_download = New-Object system.Windows.Forms.Button
$button_download.Enabled = $false
$button_download.BackColor = "#f5a623"
$button_download.text = "Download"
$button_download.width = 80
$button_download.height = 25
$button_download.location = New-Object System.Drawing.Point(345,174)
$button_download.Font = 'Microsoft Sans Serif,9,style=Bold'
$button_download.FlatStyle = "Flat"
$button_settings = New-Object system.Windows.Forms.Button
$button_settings.width = 30
$button_settings.height = 30
$button_settings.location = New-Object System.Drawing.Point(508,8)
$button_settings.image = $gear
$button_settings.FlatStyle = "Flat"
$button_settings.BackColor = "Transparent"
$button_settings.FlatAppearance.BorderSize = 0
$button_settings.FlatAppearance.MouseDownBackColor = "Transparent"
$button_settings.FlatAppearance.MouseOverBackColor = "#666666"
$groupBox_type = New-Object system.Windows.Forms.Groupbox
$groupBox_type.height = 40
$groupBox_type.width = 285
$groupBox_type.text = "Select Media Type"
$groupBox_type.location = New-Object System.Drawing.Point(140,46)
$RadioButton_movie = New-Object system.Windows.Forms.RadioButton
$RadioButton_movie.text = "Movies"
$RadioButton_movie.AutoSize = $true
$RadioButton_movie.Checked = $true
$RadioButton_movie.width = 80
$RadioButton_movie.height = 20
$RadioButton_movie.location = New-Object System.Drawing.Point(15,16)
$RadioButton_movie.Font = 'Microsoft Sans Serif,9'
$RadioButton_movie.ForeColor = "#ffffff"
$RadioButton_tv = New-Object system.Windows.Forms.RadioButton
$RadioButton_tv.text = "TV Shows"
$RadioButton_tv.AutoSize = $true
$RadioButton_tv.width = 80
$RadioButton_tv.height = 20
$RadioButton_tv.location = New-Object System.Drawing.Point(110,16)
$RadioButton_tv.Font = 'Microsoft Sans Serif,9'
$RadioButton_tv.ForeColor = "#ffffff"
$RadioButton_music = New-Object system.Windows.Forms.RadioButton
$RadioButton_music.text = "Artists"
$RadioButton_music.AutoSize = $true
$RadioButton_music.width = 80
$RadioButton_music.height = 20
$RadioButton_music.location = New-Object System.Drawing.Point(215,16)
$RadioButton_music.Font = 'Microsoft Sans Serif,9'
$RadioButton_music.ForeColor = "#ffffff"
$label_results = New-Object system.Windows.Forms.Label
$label_results.text = "Results:"
$label_results.AutoSize = $true
$label_results.width = 30
$label_results.height = 15
$label_results.location = New-Object System.Drawing.Point(15,155)
$label_results.Font = 'Microsoft Sans Serif,8'
$label_results.ForeColor = "#f5a623"
$comboBox_results = New-Object system.Windows.Forms.ComboBox
$comboBox_results.text = ""
$comboBox_results.width = 300
$comboBox_results.height = 20
$comboBox_results.location = New-Object System.Drawing.Point(15,175)
$comboBox_results.Font = 'Microsoft Sans Serif,10'
$label_seasons = New-Object system.Windows.Forms.Label
$label_seasons.text = ""
$label_seasons.AutoSize = $true
$label_seasons.width = 30
$label_seasons.height = 15
$label_seasons.location = New-Object System.Drawing.Point(15,225)
$label_seasons.Font = 'Microsoft Sans Serif,8'
$label_seasons.ForeColor = "#f5a623"
$comboBox_seasons = New-Object system.Windows.Forms.ComboBox
$comboBox_seasons.Visible = $false
$comboBox_seasons.text = ""
$comboBox_seasons.width = 130
$comboBox_seasons.height = 20
$comboBox_seasons.location = New-Object System.Drawing.Point(65,215)
$comboBox_seasons.Font = 'Microsoft Sans Serif,10'
$label_episodes = New-Object system.Windows.Forms.Label
$label_episodes.text = ""
$label_episodes.AutoSize = $true
$label_episodes.width = 30
$label_episodes.height = 15
$label_episodes.location = New-Object System.Drawing.Point(215,225)
$label_episodes.Font = 'Microsoft Sans Serif,8'
$label_episodes.ForeColor = "#f5a623"
$comboBox_episodes = New-Object system.Windows.Forms.ComboBox
$comboBox_episodes.Visible = $false
$comboBox_episodes.text = ""
$comboBox_episodes.width = 45
$comboBox_episodes.height = 20
$comboBox_episodes.location = New-Object System.Drawing.Point(265,215)
$comboBox_episodes.Font = 'Microsoft Sans Serif,10'
$label_mediaTitle = New-Object system.Windows.Forms.Label
$label_mediaTitle.text = ""
$label_mediaTitle.AutoSize = $false
$label_mediaTitle.AutoEllipsis = $true
$label_mediaTitle.width = 400
$label_mediaTitle.height = 20
$label_mediaTitle.location = $label_mediaTitle_default_xy
$label_mediaTitle.Font = 'Microsoft Sans Serif,10,style=Bold'
$label_mediaTitle.ForeColor = "#f5a623"
$label_mediaRating = New-Object system.Windows.Forms.Label
$label_mediaRating.text = ""
$label_mediaRating.AutoSize = $true
$label_mediaRating.width = 50
$label_mediaRating.height = 20
$label_mediaRating.location = $label_mediaRating_default_xy
$label_mediaRating.Font = 'Microsoft Sans Serif,9,style=Bold'
$label_mediaRating.ForeColor = "#ffffff"
$label_mediaScore = New-Object system.Windows.Forms.Label
$label_mediaScore.text = ""
$label_mediaScore.AutoSize = $true
$label_mediaScore.width = 50
$label_mediaScore.height = 20
$label_mediaScore.location = $label_mediaScore_default_xy
$label_mediaScore.Font = 'Microsoft Sans Serif,9,style=Bold'
$label_mediaScore.ForeColor = "#ffffff"
$label_mediaSummary = New-Object system.Windows.Forms.Label
$label_mediaSummary.text = $errorMsg
$label_mediaSummary.AutoSize = $false
$label_mediaSummary.AutoEllipsis = $true
$label_mediaSummary.width = 400
$label_mediaSummary.height = $label_mediaSummary_default_height
$label_mediaSummary.location = $label_mediaSummary_default_xy
$label_mediaSummary.Font = 'Microsoft Sans Serif,9'
$label_mediaSummary.ForeColor = "#ffffff"
$label_DLTitle = New-Object system.Windows.Forms.Label
$label_DLTitle.text = ""
$label_DLTitle.AutoSize = $false
$label_DLTitle.AutoEllipsis = $true
$label_DLTitle.width = 440
$label_DLTitle.height = 20
$label_DLTitle.location = New-Object System.Drawing.Point(15,430)
$label_DLTitle.Font = 'Microsoft Sans Serif,9'
$label_DLTitle.ForeColor = "#f5a623"
$label_DLProgress = New-Object system.Windows.Forms.Label
$label_DLProgress.text = ""
$label_DLProgress.AutoSize = $false
$label_DLProgress.AutoEllipsis = $true
$label_DLProgress.width = 410
$label_DLProgress.height = 20
$label_DLProgress.location = New-Object System.Drawing.Point(15,455)
$label_DLProgress.Font = 'Microsoft Sans Serif,9'
$label_DLProgress.ForeColor = "#f5a623"
$button_cancel = New-Object system.Windows.Forms.Button
$button_cancel.Visible = $false
$button_cancel.BackColor = "#f5a623"
$button_cancel.text = "Cancel"
$button_cancel.width = 80
$button_cancel.height = 25
$button_cancel.location = New-Object System.Drawing.Point(456,470)
$button_cancel.Font = 'Microsoft Sans Serif,9,style=Bold'
$button_cancel.FlatStyle = "Flat"
$checkBoxButton_pause = New-Object System.Windows.Forms.Checkbox
$checkBoxButton_pause.location = New-Object System.Drawing.Point(456,440)
$checkBoxButton_pause.Size = New-Object System.Drawing.Size(80,25)
$checkBoxButton_pause.Appearance = [System.Windows.Forms.Appearance]::Button
$checkBoxButton_pause.Visible = $false
$checkBoxButton_pause.Text = "Pause"
$checkBoxButton_pause.FlatStyle = "Flat"
$checkBoxButton_pause.BackColor = "#f5a623"
$checkBoxButton_pause.width = 80
$checkBoxButton_pause.height = 25
$checkBoxButton_pause.TextAlign = "MiddleCenter"
$checkBoxButton_pause.AllowDrop = $false
$checkBoxButton_pause.Font = 'Microsoft Sans Serif,9,style=Bold'
$checkBoxButton_pause.FlatAppearance.BorderColor = "#000000"
$progressBar = New-Object system.Windows.Forms.ProgressBar
$progressBar.Name = 'progressBar1'
$progressBar.BackColor = "#f5a623"
$progressBar.ForeColor = "#f5a623"
$progressBar.width = 430
$progressBar.height = 12
$progressBar.location = New-Object System.Drawing.Point(15,480)
$progressBar.Value = 0
$progressBar.Style = "Continuous" #"Blocks"
$progressBar.Visible = $false
$progressBar.Minimum = 0
$progressBar.Maximum = 110
# add tooltips
$toolTip = New-Object System.Windows.Forms.ToolTip
$toolTip.SetToolTip($label_search, "Searches by first Letter. Excluding 'The' and 'A'")
$form.controls.AddRange(@($groupbox_type,$label_title,$pictureBox_logo,$pictureBox_thumb,$label_search,$textBox_search,$progressBar,$button_search,$button_download,$button_settings,$label_mediaTitle,$label_mediaScore,$comboBox_results,$comboBox_seasons,$comboBox_episodes,$label_mediaRating,$label_mediaSummary,$label_results,$label_seasons,$label_episodes,$label_DLTitle,$label_DLProgress,$button_cancel,$checkBoxButton_pause))
$groupBox_type.controls.AddRange(@($RadioButton_movie,$RadioButton_tv,$RadioButton_music))
# settings form
$form2 = New-Object system.Windows.Forms.Form
$form2.ClientSize = '550,500'
$form2.text = "Saverr Settings"
$form2.BackColor = "#4a4a4a"
$form2.TopMost = $false
$form2.StartPosition = 'CenterScreen'
$form2.Icon = [System.Drawing.Icon]::FromHandle((New-Object System.Drawing.Bitmap -Argument $stream).GetHIcon())
$form2.FormBorderStyle = "FixedDialog"
$form2.MaximizeBox = $false
$form2.StartPosition = "CenterParent"
$label2_title = New-Object system.Windows.Forms.Label
$label2_title.text = "Settings"
$label2_title.AutoSize = $true
$label2_title.width = 30
$label2_title.height = 20
$label2_title.location = New-Object System.Drawing.Point(143,12)
$label2_title.Font = 'Microsoft Sans Serif,15,style=Bold'
$label2_title.ForeColor = "#f5a623"
$label2_notice = New-Object system.Windows.Forms.Label
$label2_notice.text = "Username/password is not saved.`nOnly used to retrieve Plex token"
$label2_notice.AutoSize = $true
$label2_notice.width = 70
$label2_notice.height = 20
$label2_notice.location = New-Object System.Drawing.Point(143,50)
$label2_notice.Font = 'Microsoft Sans Serif,8'
$label2_notice.ForeColor = "#ffffff"
$pictureBox2_logo = New-Object system.Windows.Forms.PictureBox
$pictureBox2_logo.width = 75
$pictureBox2_logo.height = 75
$pictureBox2_logo.location = New-Object System.Drawing.Point(15,15)
$pictureBox2_logo.image = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($plexImgDecoded.StreamSource)
$pictureBox2_logo.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::normal
$label2_username = New-Object system.Windows.Forms.Label
$label2_username.text = "Plex Username:"
$label2_username.AutoSize = $true
$label2_username.width = 30
$label2_username.height = 20
$label2_username.location = New-Object System.Drawing.Point(15,121)
$label2_username.Font = 'Microsoft Sans Serif,8'
$label2_username.ForeColor = "#ffffff"
$textBox2_username = New-Object system.Windows.Forms.TextBox
$textBox2_username.multiline = $false
$textBox2_username.width = 225
$textBox2_username.height = 20
$textBox2_username.location = New-Object System.Drawing.Point(110,110)
$textBox2_username.Font = 'Microsoft Sans Serif,10'
$label2_password = New-Object system.Windows.Forms.Label
$label2_password.text = "Plex Password:"
$label2_password.AutoSize = $true
$label2_password.width = 70
$label2_password.height = 20
$label2_password.location = New-Object System.Drawing.Point(15,166)
$label2_password.Font = 'Microsoft Sans Serif,8'
$label2_password.ForeColor = "#ffffff"
$textBox2_password = New-Object system.Windows.Forms.TextBox
$textBox2_password.PasswordChar = '*'
$textBox2_password.multiline = $false
$textBox2_password.width = 225
$textBox2_password.height = 20
$textBox2_password.location = New-Object System.Drawing.Point(110,155)
$textBox2_password.Font = 'Microsoft Sans Serif,10'
$button2_getToken = New-Object system.Windows.Forms.Button
$button2_getToken.BackColor = "#f5a623"
$button2_getToken.text = "Get Token"
$button2_getToken.width = 95
$button2_getToken.height = 25
$button2_getToken.location = New-Object System.Drawing.Point(350,154)
$button2_getToken.Font = 'Microsoft Sans Serif,9,style=Bold'
$button2_getToken.FlatStyle = "Flat"
$label2_tokenStatus = New-Object system.Windows.Forms.Label
$label2_tokenStatus.text = ""
$label2_tokenStatus.AutoSize = $true
$label2_tokenStatus.width = 70
$label2_tokenStatus.height = 20
$label2_tokenStatus.location = New-Object System.Drawing.Point(350,180)
$label2_tokenStatus.Font = 'Microsoft Sans Serif,8'
$label2_tokenStatus.ForeColor = "#ffff00"
$label2_server = New-Object system.Windows.Forms.Label
$label2_server.text = "Select Server:"
$label2_server.AutoSize = $true
$label2_server.width = 70
$label2_server.height = 20
$label2_server.location = New-Object System.Drawing.Point(15,211)
$label2_server.Font = 'Microsoft Sans Serif,8'
$label2_server.ForeColor = "#ffffff"
$comboBox2_servers = New-Object system.Windows.Forms.ComboBox
$comboBox2_servers.text = $settings.name
$comboBox2_servers.width = 225
$comboBox2_servers.height = 20
$comboBox2_servers.location = New-Object System.Drawing.Point(110,200)
$comboBox2_servers.Font = 'Microsoft Sans Serif,10'
$label2_dlPath = New-Object system.Windows.Forms.Label
$label2_dlPath.text = "Download Path:"
$label2_dlPath.AutoSize = $true
$label2_dlPath.width = 70
$label2_dlPath.height = 20
$label2_dlPath.location = New-Object System.Drawing.Point(15,256)
$label2_dlPath.Font = 'Microsoft Sans Serif,8'
$label2_dlPath.ForeColor = "#ffffff"
$textBox2_dlPath = New-Object system.Windows.Forms.TextBox
$textBox2_dlPath.text = $settings.dlPath
$textBox2_dlPath.ReadOnly = $true
$textBox2_dlPath.multiline = $false
$textBox2_dlPath.width = 225
$textBox2_dlPath.height = 20
$textBox2_dlPath.location = New-Object System.Drawing.Point(110,245)
$textBox2_dlPath.Font = 'Microsoft Sans Serif,10'
$textBox2_dlPath.Enabled = $false
$button2_dlPath = New-Object system.Windows.Forms.Button
$button2_dlPath.BackColor = "#f5a623"
$button2_dlPath.text = "Select Path"
$button2_dlPath.width = 95
$button2_dlPath.height = 25
$button2_dlPath.location = New-Object System.Drawing.Point(350,244)
$button2_dlPath.Font = 'Microsoft Sans Serif,9,style=Bold'
$button2_dlPath.FlatStyle = "Flat"
$label2_pathStatus = New-Object system.Windows.Forms.Label
$label2_pathStatus.text = ""
$label2_pathStatus.AutoSize = $true
$label2_pathStatus.width = 70
$label2_pathStatus.height = 20
$label2_pathStatus.location = New-Object System.Drawing.Point(350,270)
$label2_pathStatus.Font = 'Microsoft Sans Serif,8'
$label2_pathStatus.ForeColor = "#ffff00"
$label2_ssl = New-Object system.Windows.Forms.Label
$label2_ssl.text = "SSL Required:"
$label2_ssl.AutoSize = $true
$label2_ssl.width = 70
$label2_ssl.height = 20
$label2_ssl.location = New-Object System.Drawing.Point(15,290)
$label2_ssl.Font = 'Microsoft Sans Serif,8'
$label2_ssl.ForeColor = "#ffffff"
$checkBox_ssl = New-Object System.Windows.Forms.Checkbox
$checkBox_ssl.location = New-Object System.Drawing.Point(110,285)
$checkBox_ssl.Size = New-Object System.Drawing.Size(80,25)
$checkBox_ssl.width = 80
$checkBox_ssl.height = 25
$checkBox_ssl.checked = $settings.ssl
$label2_debug = New-Object system.Windows.Forms.Label
$label2_debug.text = "Debug Logging:"
$label2_debug.AutoSize = $true
$label2_debug.width = 70
$label2_debug.height = 20
$label2_debug.location = New-Object System.Drawing.Point(15,325)
$label2_debug.Font = 'Microsoft Sans Serif,8'
$label2_debug.ForeColor = "#ffffff"
$checkBox_debug = New-Object System.Windows.Forms.Checkbox
$checkBox_debug.location = New-Object System.Drawing.Point(110,320)
$checkBox_debug.Size = New-Object System.Drawing.Size(80,25)
$checkBox_debug.width = 80
$checkBox_debug.height = 25
$checkBox_debug.checked = $settings.logging
$label2_ssl_info = New-Object system.Windows.Forms.Label
$label2_ssl_info.text = "[Restart Saverr after changing SSL or Debug options]"
$label2_ssl_info.AutoSize = $true
$label2_ssl_info.width = 70
$label2_ssl_info.height = 20
$label2_ssl_info.location = New-Object System.Drawing.Point(15,360)
$label2_ssl_info.Font = 'Microsoft Sans Serif,8'
$label2_ssl_info.ForeColor = "#ffffff"
$button2_servers = New-Object system.Windows.Forms.Button
$button2_servers.BackColor = "#f5a623"
$button2_servers.text = "List Servers"
$button2_servers.width = 95
$button2_servers.height = 25
$button2_servers.location = New-Object System.Drawing.Point(350,199)
$button2_servers.Font = 'Microsoft Sans Serif,9,style=Bold'
$button2_servers.FlatStyle = "Flat"
$label2_serverStatus = New-Object system.Windows.Forms.Label
$label2_serverStatus.text = ""
$label2_serverStatus.AutoSize = $true
$label2_serverStatus.width = 70
$label2_serverStatus.height = 20
$label2_serverStatus.location = New-Object System.Drawing.Point(350,225)
$label2_serverStatus.Font = 'Microsoft Sans Serif,8'
$label2_serverStatus.ForeColor = "#ffff00"
$label2_notice2 = New-Object system.Windows.Forms.Label
$label2_notice2.text = ""
$label2_notice2.AutoSize = $true
$label2_notice2.width = 70
$label2_notice2.height = 20
$label2_notice2.location = New-Object System.Drawing.Point(15,395)
$label2_notice2.Font = 'Microsoft Sans Serif,8'
$label2_notice2.ForeColor = "#ffffff"
$label2_saveStatus = New-Object system.Windows.Forms.Label
$label2_saveStatus.text = ""
$label2_saveStatus.AutoSize = $true
$label2_saveStatus.width = 70
$label2_saveStatus.height = 20
$label2_saveStatus.location = New-Object System.Drawing.Point(110,275)
$label2_saveStatus.Font = 'Microsoft Sans Serif,8'
$label2_saveStatus.ForeColor = "#00ff00"
$label2_help = New-Object system.Windows.Forms.LinkLabel
$label2_help.text = "Help"
$label2_help.AutoSize = $true
$label2_help.width = 70
$label2_help.height = 20
$label2_help.location = New-Object System.Drawing.Point(15,480)
$label2_help.Font = 'Microsoft Sans Serif,9'
$label2_help.ForeColor = "#00ff00"
$label2_help.LinkColor = "#f5a623"
$label2_help.ActiveLinkColor = "#f5a623"
$label2_help.add_Click({[system.Diagnostics.Process]::start("https://github.com/ninthwalker/saverr")})
$label2_version = New-Object system.Windows.Forms.Label
$label2_version.text = "Ver. 1.1.2"
$label2_version.AutoSize = $true
$label2_version.width = 70
$label2_version.height = 20
$label2_version.location = New-Object System.Drawing.Point(480,480)
$label2_version.Font = 'Microsoft Sans Serif,9'
$label2_version.ForeColor = "#f5a623"
$form2.controls.AddRange(@($label2_title,$label2_username,$label2_password,$label2_dlPath,$label2_server,$label2_pathStatus,$label2_serverStatus,$label2_notice,$label2_notice2,$label2_saveStatus,$label2_tokenStatus,$label2_help,$label2_version,$textBox2_username,$textBox2_password,$textBox2_dlPath,$label2_debug,$checkBox_debug,$label2_ssl,$checkBox_ssl,$label2_ssl_info,$comboBox2_servers,$button2_servers,$pictureBox2_logo,$button2_getToken,$button2_dlPath))
############################## CODE ################################
# search server for media. Movies/tv are by firstletter. Music is by artist.
function search {
Try {
# clear old searches
clearMediaInfo
clearDLStatus
$comboBox_results.Text = "Searching ..."
# get sections
$sections = $scheme + $settings.server + "/library/sections/" + "?X-Plex-Token=" + $settings.serverToken
$xmlsearch = plx $sections
#$sectionType = $groupBox_type.Controls | ? { $_.Checked } | Select-Object Text
# GUI Selection of 'type' to search for. Remove spaces and leading 'the' and 'a' since plex removes those from search words
$searchName = ($textBox_search.Text).TrimStart().TrimEnd()
$searchName = $searchName -replace '^the |^a ', ''
$sectionType = $groupBox_type.Controls | ? { $_.Checked -eq $true} | Select-Object Text
Switch ($sectionType.Text)
{
'Movies' {$script:type = "movie"
$subSection = "Video"; Break
}
'TV Shows' {$script:type = "show"
$subSection = "Directory"; Break
}
'Artists' {$script:type = "artist"
$subSection = "Directory"; Break
}
}
# get 'type' of key
$sections2search = $xmlsearch.MediaContainer.Directory | ? {$_.type -eq $type} | select key
$firstChar = $searchName.ToUpper()[0]
# Search movies/tv
if ($type -ne "artist") {
$sectionsList = new-object collections.generic.list[object]
foreach ($section in $sections2search) {
$sectionsUrl = $scheme + $settings.server + "/library/sections/$($section.key)/firstCharacter/$firstChar/" + "?X-Plex-Token=" + $settings.serverToken
$sectionsList.Add((plx $sectionsUrl))
}
# search through list for match
$script:searchResults = $sectionsList.MediaContainer.$subSection | ? {$_.title -like "*$searchName*" -and $_.type -eq $type} | select title,type,key,tagline,summary,year,contentrating,thumb,rating
}
#search artists
else {
$artistList = new-object collections.generic.list[object]
foreach ($section in $sections2search) {
$sectionsUrl = $scheme + $settings.server + "/library/sections/$($section.key)/all" + "?X-Plex-Token=" + $settings.serverToken
$artistList.Add((plx $sectionsUrl))
}
$script:searchResults = $artistList.MediaContainer.$subSection | ? {$_.title -like "*$searchName*" -and $_.type -eq $type} | select title,type,key,thumb
# search through list for match
$trackList = new-object collections.generic.list[object]
$artistPath = $artistsList.MediaContainer.$subSection | select key
foreach ($artist in $artistPath) {
$artistURL = $scheme + $settings.server + "$($artist.key)" + "?X-Plex-Token=" + $settings.serverToken
$trackList.Add((plx $artistURl))
}
}
#show results
if ($searchresults) {
$comboBox_results.Text = "$(@($searchResults).count) $($sectionType.Text) found!"
$comboBox_results.Text = "$(@($searchResults).count) $($sectionType.Text) found!"
foreach ($item in $searchresults) {
if ($type -ne "artist") {
if ($item.year) {
$comboBox_results.Items.Add($item.title + ' (' + $item.year + ')')
}
else {
$comboBox_results.Items.Add($item.title)
}
}
else {
$comboBox_results.Items.Add($item.title)
}
}
}
else {
$ComboBox_results.Text = "No results found!"
}
}
Catch {
logit
$comboBox_results.Items.Clear()
$ComboBox_results.Text = "Error! Check settings/token/server status?"
}
}
function mediaInfo {
clearDLStatus
$comboBox_index = $comboBox_results.SelectedIndex
$script:info = $searchResults[$comboBox_index] | Select title,type,key,tagline,summary,year,contentrating,thumb,rating,size
$thumb = $scheme + $settings.server + $info.thumb + "?X-Plex-Token=" + $settings.serverToken
# enable download button if no other downloads in progress
if (!(Get-BitsTransfer)) {
$button_download.Enabled = $true
}
if ($info.thumb) {
$pictureBox_thumb.ImageLocation = "$thumb"
$pictureBox_thumb.Visible = $true
}
else {
$pictureBox_thumb.ImageLocation = ""
$pictureBox_thumb.Image = $loading
$pictureBox_thumb.Visible = $true
}
if ($info.year) {
$label_mediaTitle.Text = "$($info.title) ($($info.year))"
}
else {
$label_mediaTitle.Text = "$($info.title)"
}
if ($info.contentrating) {
$label_mediaRating.Text = "$($info.contentrating)"
}
elseif ($type -ne "artist") {
$label_mediaRating.Text = "No Rating"
}
if ($info.rating) {
$label_mediaScore.Text = "$($info.rating)"
}
if ($info.summary) {
$label_mediaSummary.Text = "$($info.summary)"
}
# show season/ep boxes
if ($type -eq "show") {
#get seasons
$comboBox_seasons.Items.Clear()
$seasonPath = $scheme + $settings.server + "$($info.key)" + "?X-Plex-Token=" + $settings.serverToken
$xmlSeason = plx $seasonPath
$script:seasons = $xmlSeason.MediaContainer.directory | select title,key,index
foreach ($season in $seasons) {
$comboBox_seasons.Items.Add($season.title)
}
if (($comboBox_seasons.Items).count -ne "1" ) {
$comboBox_seasons.Text = $comboBox_seasons.items[1]
}
else {
$comboBox_seasons.Text = $comboBox_seasons.items[0]
}
}
if ($type -eq "artist") {
#get albums
$comboBox_seasons.Items.Clear()
$seasonPath = $scheme + $settings.server + "$($info.key)" + "?X-Plex-Token=" + $settings.serverToken
$xmlSeason = plx $seasonPath
$script:seasons = $xmlSeason.MediaContainer.directory | select title,key,index,year
foreach ($season in $seasons) {
$comboBox_seasons.Items.Add($season.title)
}
if (($comboBox_seasons.Items).count -ge 2) {
$comboBox_seasons.Items.Add("All Albums")
}
$comboBox_seasons.Text = $comboBox_seasons.items[0]
}
}
#get episodes/tracks
function episodeSelection {
clearDLStatus
# Movies and tv shows
if ($type -ne "artist") {
$comboBox_episodes.Items.Clear()
$script:comboBox_seasons_index = $comboBox_seasons.SelectedIndex
if ($comboBox_seasons.Text -ne "All Episodes") {
$episodePath = $scheme + $settings.server + "$($seasons[$comboBox_seasons_index].key)" + "?X-Plex-Token=" + $settings.serverToken
$script:xmlEpisode = plx $episodePath
$script:episodes = $xmlEpisode.MediaContainer.video | select title,key,contentrating,summary,rating,year,thumb,originallyAvailableAt,index,duration
foreach ($episode in $episodes) {
$comboBox_episodes.Items.Add($episode.index)
}
if ($comboBox_episodes.Items -ge 2) {
$comboBox_episodes.Items.Add("All")
}
$comboBox_episodes.Text = $comboBox_episodes.items[0]
}
else {
$comboBox_episodes.Text = "All"
$label_mediaTitle.Text = "Download All Episodes from All Seasons"
$label_mediaRating.Text = ""
$label_mediaScore.Text = ""
$label_mediaSummary.Text = "Notice: This may take a very long time depending on number of seasons/episodes."
}
}
# music
else {
$comboBox_episodes.Items.Clear()
$comboBox_seasons_index = $comboBox_seasons.SelectedIndex
$episodePath = $scheme + $settings.server + "$($seasons[$comboBox_seasons_index].key)" + "?X-Plex-Token=" + $settings.serverToken
$script:xmlEpisode = plx $episodePath
$script:episodes = $xmlEpisode.MediaContainer.track | select title,key,index,duration,summary,parentYear,thumb,grandparentthumb,addedAt
foreach ($episode in $episodes) {
$comboBox_episodes.Items.Add($episode.title)
}
if (($comboBox_episodes.Items).count -ge 2) {
$comboBox_episodes.Items.Add("All Tracks")
}
if ($comboBox_seasons.text -ne "All Albums") {
$comboBox_episodes.Text = $comboBox_episodes.items[0]
}
else {
$comboBox_episodes.Text = "All Tracks"
$label_mediaTitle.Text = "Download All Albums"
$label_mediaRating.Text = ""
$label_mediaScore.Text = ""
$label_mediaSummary.Text = "Notice: This may take a very long time depending on number of albums/tracks."
}
}
}
function mediaEpInfo {
clearDLStatus
$comboBox_episode_index = $comboBox_episodes.SelectedIndex
if ($comboBox_episode_index -ne $null -and $episodes -ne $null) {
$script:infoEp = $episodes[$comboBox_episode_index] | Select title,type,key,tagline,summary,year,contentrating,thumb,rating
}
if ($comboBox_episodes.Text -eq "All" -and $comboBox_seasons.Text -ne "All episodes") {
$label_mediaTitle.Text = "Download All Episodes from $($comboBox_seasons.Text)"
$label_mediaRating.Text = ""
$label_mediaScore.Text = ""
$label_mediaSummary.Text = "Notice: This may take a very long time depending on number of episodes."
}
elseif ($comboBox_episodes.Text -eq "All Tracks" -and $comboBox_seasons.Text -ne "All Albums") {
$label_mediaTitle.Text = "Download All Tracks from $($comboBox_seasons.Text)"
$label_mediaRating.Text = ""
$label_mediaScore.Text = ""
$label_mediaSummary.Text = "Notice: This may take a very long time depending on number of tracks."
}
else {
if ($infoEp.year) {
$label_mediaTitle.Text = "$($infoEp.title) ($($infoEp.year))"
}
else {
$label_mediaTitle.Text = "$($infoEp.title)"
}
if ($infoEp.contentrating) {
$label_mediaRating.Text = "$($infoEp.contentrating)"
}
elseif ($type -ne "artist") {
$label_mediaRating.Text = "No Rating"
}
if ($infoEp.rating) {
$label_mediaScore.Text = "$($infoEp.rating)"
}
if ($infoEp.summary) {
$label_mediaSummary.Text = "$($infoEp.summary)"
}
else {
$label_mediaSummary.Text = ""
}
}
}
# retrieve token. Does not store username/password, only the token
function getToken {
try {
$label2_tokenStatus.ForeColor = "#ffff00"
$label2_tokenStatus.Text = "Retrieving ..."
$username = $textBox2_username.Text
$password = $textBox2_password.Text
# Use this method for now for more powershell version backwards compatability instead of -credential
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
$headers = @{
"X-Plex-Version" = "1.1.2"
"X-Plex-Product" = "Saverr"
"X-Plex-Client-Identifier" = "271938"
"Content-Type" = "application/xml"
"Authorization" = ("Basic {0}" -f $base64AuthInfo)
}
$data = Invoke-RestMethod -Method POST -Uri $plexSignInUrl -Headers $headers
$script:userToken = $data.user.authToken
# update settings file
if (Test-Path .\saverrSettings.xml) {
$script:settings = Import-Clixml .\saverrSettings.xml
Add-Member -InputObject $settings -MemberType NoteProperty -Name 'userToken' -Value $userToken -force
$settings | Export-Clixml .\saverrSettings.xml
}
else {
$script:settings = [pscustomobject] @{
userToken = $userToken
}
$settings | Export-Clixml .\saverrSettings.xml
}
$settings = Import-Clixml .\saverrSettings.xml
$label2_tokenStatus.ForeColor = "#00ff00"
$label2_tokenStatus.Text = "Token Saved!"
}
catch {
logit
$label2_tokenStatus.ForeColor = "#ff0000"
$label2_tokenStatus.Text = "Error! User/Pass?"
}
}
function getServers {
Try {
$label2_serverStatus.text = "Searching ..."
$label2_notice2.text = ""
# full server url
$serversUrl = $plexServersUrl + "?X-Plex-Token=" + $settings.userToken
# get servers
$serversXml = plx $serversUrl
$script:serverList = $serversxml.MediaContainer.Server | select name,host,port,accessToken,localAddresses,owned
#output servers
if ($serverList) {
$comboBox2_servers.Items.Clear();
$comboBox2_servers.Text = "$(@($serverList).count) Plex Servers found!"
foreach ($server in $serverList) {
$comboBox2_servers.Items.Add($server.Name)
}
}
else {
$comboBox2_servers.Items.Clear()
$ComboBox2_servers.Text = "No Plex servers found! Got Token?"
}
$label2_serverStatus.text = ""
}
Catch {
logit
$label2_serverStatus.text = ""
$comboBox2_servers.Items.Clear()
$ComboBox2_servers.Text = "Error! Check token?"
}
}
# save all setttings to file
function saveServer {
Try {
$comboBox2_index = $comboBox2_servers.SelectedIndex
$selectedServer = $serverList[$comboBox2_index]
if ($selectedServer.owned -ne "1") {
$serverUrl = $selectedServer.Host + ":" + $selectedServer.Port
}
else {
$ipCheck = Invoke-RestMethod http://ipinfo.io/json | Select -exp ip
if ($ipcheck -eq $selectedServer.Host) {
if ( (($selectedServer.localaddresses).GetType().name) -eq "String" ) {
$serverUrl = $selectedServer.localAddresses + ":" + "32400"
}
elseif ( (($selectedServer.localaddresses).GetType().name) -eq "Array" ) {
$serverUrl = $selectedServer.localAddresses[0] + ":" + "32400"
}
}
else {
$serverUrl = $selectedServer.Host + ":" + $selectedServer.Port
}
}
$serverToken = $($selectedServer.accessToken)
$serverName = $($selectedServer.name)
[PsCustomObject] @{
name = "$serverName"
server = "$serverUrl"
dlPath = "$($textBox2_dlPath.Text)"
userToken = "$($settings.userToken)"
serverToken = "$serverToken"
ssl = "$ssl"
logging = "$debug"
} | Export-Clixml .\saverrSettings.xml
$label2_serverStatus.ForeColor = "#00ff00"
$label2_serverStatus.text = "Server Saved!"
}
Catch {
logit
$label2_notice2.ForeColor = "#ff0000"
$label2_notice2.text = "Error saving! Got token? selected a server?"
}
}
function clearStatusSave {
# save path
# $dlPath = "$($textBox2_dlPath.Text)" # old way before dialog box
if (Test-Path .\saverrSettings.xml) {
$script:settings = Import-Clixml .\saverrSettings.xml
if ($dlPath) {
Add-Member -InputObject $settings -MemberType NoteProperty -Name 'dlPath' -Value $dlPath -force
$settings | Export-Clixml .\saverrSettings.xml
}
}
else {
if ($dlPath) {
$script:settings = [pscustomobject] @{
dlPath = $dlPath
}
}
}
# clear status
clearMediaInfo
clearDLStatus
$errorMsg = ""
$label2_saveStatus.text = ""
$label2_serverStatus.text = ""
$label2_tokenStatus.Text = ""
$label2_pathStatus.Text = ""
$label2_notice2.Text = ""
$textBox_search.Text = ""
$textBox2_dlPath.Text = $settings.dlPath
$textBox2_username.Text = ""
$textBox2_password.Text = ""
$comboBox2_servers.Items.Clear()
$comboBox2_servers.Text = $settings.name
Try {
if ($dlPath) {
New-Item -ItemType Directory -Force -Path $dlPath
}
$label_mediaTitle.ForeColor = "#ffffff"
$label_mediaTitle.Text = ""
$label_mediaSummary.Text = ""
}
Catch {
logit
$label_mediaTitle.ForeColor = "#ff0000"
$label_mediaTitle.Text = "Error creating download Path"
$label_mediaSummary.Text = "Could Not validate download directory:`n$dlPath.`n`nCheck path name or system permissions maybe?"
}
Try {
if ($settings) {
$settings | Export-Clixml .\saverrSettings.xml
}
$label_mediaTitle.ForeColor = "#ffffff"
$label_mediaTitle.Text = ""
$label_mediaSummary.Text = ""
}
Catch {
logit
$label_mediaTitle.ForeColor = "#ff0000"
$label_mediaTitle.Text = "Error saving settings"
$label_mediaSummary.Text = "Could Not Create settings file at:`n$PSScriptRoot.`n`nCheck path or system permissions maybe?"
}
if ((!($settings.name)) -or (!($settings.server)) -or (!($settings.userToken)) -or (!($settings.serverToken)) -or (!($settings.dlPath))) {
$label_mediaSummary.text = "Settings are not fully configured.`nPlease click the gear icon before searching."
}
else {
$label_mediaSummary.text = ""
}
}
function clearMediaInfo {
$comboBox_results.Items.Clear()
$comboBox_results.Text = ""
$pictureBox_thumb.Visible = $false
$pictureBox_thumb.image = $loading
$pictureBox_thumb.ImageLocation = ""
$label_mediaTitle.Text = ""
$label_mediaRating.Text = ""
$label_mediaScore.Text = ""
$label_mediaSummary.Text = ""
$comboBox_seasons.Text = ""
$comboBox_episodes.Text = ""
$comboBox_seasons.Items.Clear()
$comboBox_episodes.Items.Clear()
$button_download.Enabled = $false
}
function clearDLStatus {
if ($label_DLTitle.Text -match "^Download Completed|^Download Failed|^Download Cancelled|^There was an error|^Error") {
$label_DLTitle.Text = ""
}
if ($label_DLProgress.Text -ne "Download Paused!") {
$label_DLProgress.Text = ""
$label_DLProgress.ForeColor = "#f5a623"
}
}
function cancelJob {
$progressBar.Value = 0
$label_DLTitle.ForeColor = "#ff0000"
$label_DLTitle.Text = "Download Cancelled!"
$label_DLProgress.Text = ""
$progressBar.Visible = $false
if ($comboBox_results.Items) {
$button_download.Enabled = $true
}
$button_cancel.Visible = $false
$button_cancel.Enabled = $false
$CheckBoxButton_pause.Visible = $false
$CheckboxButton_pause.Enabled = $false
$script:pauseLoop = $false
$checkBoxButton_pause.Text = "Pause"
Get-BitsTransfer | Complete-BitsTransfer
# clean up any empty folders created.
# This can throw an error in the console sometimes if the path is deleted too fast and then it doesn't exist. Not worth it to remove error, doesn't stop the app.
if ($dlType -eq "allEp" -or $dlType -eq "allSeasons") {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
# enable minimize again
$form.MinimizeBox = $true
}
########### OPERATIONS ###############
# Buttons
$button_download.Add_Click({
Try {
if ((Get-BitsTransfer).JobState -ne "Suspended") {
# tell them what's going on, reset status
$button_download.Enabled = $false
$label_DLTitle.ForeColor = "#f5a623"
$label_DLProgress.ForeColor = "#f5a623"
$label_DLProgress.Text = ""
$label_DLTitle.Text = "Processing download request"
$label_DLTitle.Refresh()
$button_download.Text = "Download"
$checkBoxButton_pause.Checked = $false
$status = "success"
# movie dl links
if ($type -eq "movie") {
$mediaURL = $scheme + $settings.server + $info.key + "?X-Plex-Token=" + $settings.serverToken
$mediaPath = plx $mediaURL
$mediaInfo = $mediaPath.MediaContainer.Video.Media.Part | select key,file -First 1
$dlURL = $scheme + $settings.server + $mediaInfo.key + "?download=1" + "&X-Plex-Token=" + $settings.serverToken
$script:dlName = Split-Path $mediaInfo.file -Leaf
$script:dlType = "one"
}
# tv show dl links
if ($type -eq "show") {
# if all episodes from one season is selected
if ($comboBox_episodes.Text -eq "All" -and $comboBox_seasons.Text -ne "All episodes") {
$script:allSeasonPath = "$($settings.dlPath)\$(Remove-InvalidChars $info.title)"
$allEpPath = "$allSeasonPath\$(Remove-InvalidChars $comboBox_seasons.Text)"
New-Item -ItemType Directory -Force -Path $allEpPath
$allEp = $xmlepisode.MediaContainer.Video.Media.Part | select @{n="Source";e={$scheme + $settings.server + $_.key + "?X-Plex-Token=" + $settings.serverToken}},@{n="Destination";e={$allEpPath + "\" + (Split-Path $_.file -Leaf)}}
$script:dlType = "allEp"
# remove links that have already been downloaded
$allEpData = @()
For ($I=0; $I -lt $allep.count; $I++) {
if (!(Test-Path $allEp.destination[$I])) {
$allEpData += [pscustomobject] @{
Source = $allep.source[$I]
Destination = $allEp.destination[$I]
}
}
}
}
# if all seasons and all episodes is selected
elseif ($comboBox_episodes.Text -eq "All" -and $comboBox_seasons.Text -eq "All episodes") {
$script:allSeasonPath = "$($settings.dlPath)\$(Remove-InvalidChars $info.title)"
$mediaURL = $scheme + $settings.server + $seasons.key[0] + "?X-Plex-Token=" + $settings.serverToken
$mediaPath = plx $mediaURL
$seasonNumber = $mediaPath.MediaContainer.Video | select parenttitle
$seasonClean = $seasonNumber.parenttitle | % {Remove-InvalidChars $_}
$allEp = $mediaPath.MediaContainer.Video.Media.Part | select @{n="Source";e={$scheme + $settings.server + $_.key + "?X-Plex-Token=" + $settings.serverToken}},@{n="Destination";e={(Split-Path $_.file -Leaf)}}
$allEpClean = $allEp.destination | % {Remove-InvalidChars $_}
$script:dlType = "allSeasons"
# combine source/destination/season data for Bitstransfer import
# remove links that have already been downloaded
$allEpData = @()
For ($I=0; $I -lt $allep.count; $I++) {
$finalDestination = $allSeasonPath + "\" + $seasonClean[$I] + "\" + $allEpClean[$I]
if (!(Test-Path $finalDestination)) {
$allEpData += [pscustomobject] @{
Source = $allep.source[$I]
Destination = $finalDestination
}
}
}
# Bitstransfer defaults to 200 max files per job. Truncate download to 200 unless registry value is set to something else.
if ($allEpData.length -ge $limit) {
$setLimit = $limit - 1
$allEpData = $allEpData[0..$setLimit]
$noLimitStatus = "(Max limit set to $limit)"
}
# pre-create directories for seasons
$seasonClean | select -Unique | % {New-Item -ItemType Directory -Force -Path "$allSeasonPath\$_"}
}
# if just one episode or a movie is selected
else {
$mediaURL = $scheme + $settings.server + $infoEp.key + "?X-Plex-Token=" + $settings.serverToken
$mediaPath = plx $mediaURL
$mediaInfo = $mediaPath.MediaContainer.Video.Media.Part | select key,file -First 1
$dlURL = $scheme + $settings.server + $mediaInfo.key + "?download=1" + "&X-Plex-Token=" + $settings.serverToken
$script:dlName = Split-Path $mediaInfo.file -Leaf | % {Remove-InvalidChars $_}
$script:dlType = "one"
}
}
# music dl links
if ($type -eq "artist") {
# if all tracks from one album is selected
if ($comboBox_episodes.Text -eq "All Tracks" -and $comboBox_seasons.Text -ne "All Albums") {
$script:allSeasonPath = "$($settings.dlPath)\$(Remove-InvalidChars $info.title)"
$allEpPath = "$allSeasonPath\$(Remove-InvalidChars $comboBox_seasons.Text)"
New-Item -ItemType Directory -Force -Path $allEpPath
$allEp = $xmlepisode.MediaContainer.Track.Media.Part | select @{n="Source";e={$scheme + $settings.server + $_.key + "?X-Plex-Token=" + $settings.serverToken}},@{n="Destination";e={$allEpPath + "\" + (Split-Path $_.file -Leaf)}}
$script:dlType = "allTracks"
# remove links that have already been downloaded
$allEpData = @()
For ($I=0; $I -lt $allep.count; $I++) {
if (!(Test-Path $allEp.destination[$I])) {
$allEpData += [pscustomobject] @{
Source = $allep.source[$I]
Destination = $allEp.destination[$I]
}
}
}
}
# if all tracks and all albums is selected
elseif ($comboBox_episodes.Text -eq "All Tracks" -and $comboBox_seasons.Text -eq "All Albums") {
$script:allSeasonPath = "$($settings.dlPath)\$(Remove-InvalidChars $info.title)"
# collect all album metadata paths
$mediaURL = @()
$seasons | % { $mediaURL += $scheme + $settings.server + $_.key + "?X-Plex-Token=" + $settings.serverToken }
# get all tracks
$all = $mediaURL | % {(plx $_).MediaContainer.Track}
$allEp = $all.media.part | select @{n="Source";e={$scheme + $settings.server + $_.key + "?X-Plex-Token=" + $settings.serverToken}},@{n="Destination";e={(Split-Path $_.file -Leaf)}}
$allClean = $all.parenttitle | % {Remove-InvalidChars $_}
$allSeasonClean = $all.parenttitle | % {Remove-InvalidChars $_}
$allEpClean = $allEp.destination | % {Remove-InvalidChars $_}
$script:dlType = "allAlbums"
# combine source/destination/season data for Bitstransfer import
$allEpData = @()
For ($I=0; $I -lt $allEp.count; $I++) {
$finalDestination = $allSeasonPath + "\" + $allSeasonClean[$I] + "\" + $allEpClean[$I]
if (!(Test-Path $finalDestination)) {
$allEpData += [pscustomobject] @{
Source = $allEp.source[$I]
Destination = $finalDestination
}
}
}
# Bitstransfer defaults to 200 max files per job. Truncate download to 200 unless registry value is set to something else.
if ($allEpData.length -ge $limit) {
$setLimit = $limit - 1
$allEpData = $allEpData[0..$setLimit]
$noLimitStatus = "(Max limit set to $limit)"
}
# pre-create directories for seasons
$allClean | select -Unique | % {New-Item -ItemType Directory -Force -Path "$allSeasonPath\$_"}
}
# if just one music track is selected
else {
$mediaURL = $scheme + $settings.server + $infoEp.key + "?X-Plex-Token=" + $settings.serverToken
$mediaPath = plx $mediaURL
$mediaInfo = $mediaPath.MediaContainer.track.media.part | select key,file -First 1
$mediaInfo2 = $mediaPath.MediaContainer.track | select grandparentTitle,parentTitle,title -First 1
$dlURL = $scheme + $settings.server + $mediaInfo.key + "?download=1" + "&X-Plex-Token=" + $settings.serverToken
$script:dlName = Split-Path $mediaInfo.file -Leaf | % {Remove-InvalidChars $_}
$script:dlType = "one"
}
}
# Cancelling all old Bits jobs
Get-BitsTransfer | Remove-BitsTransfer
# get starting time
$startTime = Get-Date
# disable minimize since it causes issues during downloads
$form.MinimizeBox = $false
# download all episodes from a season or album
if ($dlType -eq "allEp" -or $dlType -eq "allTracks") {
$script:myjob = Start-BitsTransfer -source "$($allEpData.Source[0])" -Destination "$($allEpData.Destination[0])" -DisplayName "Downloading ..." -Description "All Episodes" -Asynchronous -Suspended
$allEpData[1..($allEpData.Length -1)] | Add-BitsFile $myjob
if ($ssl -eq $True) {bitsadmin /SetSecurityFlags $myjob.displayname 30}
Resume-BitsTransfer $myjob -Asynchronous
}
# download all seasons or all albums
elseif ($dltype -eq "allSeasons" -or $dlType -eq "allAlbums") {
$script:myjob = Start-BitsTransfer -source "$($allEpData.Source[0])" -Destination "$($allEpData.Destination[0])" -DisplayName "Downloading ..." -Description "All Episodes" -Asynchronous -Suspended
$allEpData[1..($allEpData.Length -1)] | Add-BitsFile $myjob
if ($ssl -eq $True) {bitsadmin /SetSecurityFlags $myjob.displayname 30}
Resume-BitsTransfer $myjob -Asynchronous
}
# download a movie or one episode or one song
else {
$script:myjob = Start-BitsTransfer -Source $dlURL -Destination "$($settings.dlPath)\$dlName" -DisplayName "Downloading ..." -Description $dlName -Asynchronous -Suspended
if ($ssl -eq $True) {bitsadmin /SetSecurityFlags $myjob.displayname 30}
Resume-BitsTransfer $myjob -Asynchronous
}
}
elseif ((Get-BitsTransfer).JobState -eq "Suspended") {
# resume if they paused the download
$button_download.Enabled = $false
$button_download.Text = "Download"
$label_DLTitle.ForeColor = "#f5a623"
$label_DLProgress.ForeColor = "#f5a623"
$label_DLTitle.Text = "Paused download detected. Resuming progress ..."
$checkBoxButton_pause.Text = "Pause"
$label_DLProgress.Text = ""
$label_DLProgress.ForeColor = "#f5a623"
Resume-BitsTransfer $myjob -Asynchronous
$label_DLTitle.Refresh()
$status = "success"
}
# disable minimize since it causes issues during downloads
$form.MinimizeBox = $false
# Pause to let it start before checking progress. Timeout after 30sec
$count = 0
$noDot = (0,4,8,12,16,20,24,28)
$oneDot = (1,5,9,13,17,21,25,29)
$twoDot = (2,6,10,14,18,22,26,30)
$threeDot = (3,7,11,15,19,23,27)
# timeout
:check while ($count -lt $timeout) {
if (((Get-BitsTransfer | ? { $_.JobState -eq "Transferring" }).Count -gt 0) -or (Get-BitsTransfer | ? { $_.JobState -eq "Transferred" }) -or (Get-BitsTransfer | ? { $_.JobState -eq "Error" })) {
# exit check
break check
}
if ($noDot -contains $count) {
$label_DLTitle.Text = "Processing download request"
$label_DLTitle.Refresh()
}
elseif ($oneDot -contains $count) {
$label_DLTitle.Text = "Processing download request ."
$label_DLTitle.Refresh()
}
elseif ($twoDot -contains $count) {
$label_DLTitle.Text = "Processing download request . ."
$label_DLTitle.Refresh()
}
elseif ($threeDot -contains $count) {
$label_DLTitle.Text = "Processing download request . . ."
$label_DLTitle.Refresh()
}
# increase counter
Start-Sleep -Seconds 1
$count++
}
if ($count -ge $timeout) {
$status = "failed"
if (Get-BitsTransfer | ? { $_.JobState -like "*Error*" }) {
$bitsError = (Get-BitsTransfer | select ErrorDescription).ErrorDescription
if ($debug) {
$eMSG = "$(Get-Date): Connection Timed out after $timeout seconds. $bitsError"
$eMSG | Out-File ".\saverrLog.txt" -Append
}
}
# remove bitstransfer jobs and clean up empty directories created
Get-BitsTransfer | Remove-BitsTransfer
if ($dlType -like "all*") {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
$label_DLTitle.ForeColor = "#ff0000"
$label_DLTitle.Text = "Download Failed! Timed out.`n Error: $bitsError"
$button_download.Enabled = $true
$label_DLProgress.Text = ""
}
if ((Get-BitsTransfer | select ErrorDescription).ErrorDescription -like "*404*") {
$status = "failed"
# remove bitstransfer jobs and clean up empty directories created
Get-BitsTransfer | Remove-BitsTransfer
if ($dlType -like "all*") {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
$label_DLTitle.ForeColor = "#ff0000"
$label_DLTitle.Text = "Download Failed! File not found. Check server"
$button_download.Enabled = $true
$label_DLProgress.Text = ""
}
if ($status -eq "success") {
# Show progress
$progressBar.Visible = $true
$progressBar.Value = 0
$label_DLProgress.Text = ""
$label_DLProgress.ForeColor = "#f5a623"
# Init CancelLoop
$script:cancelLoop = $false
$button_cancel.Enabled = $true
$button_cancel.Visible = $true
$checkBoxButton_pause.Enabled = $true
$checkBoxButton_pause.Visible = $true
if (Get-BitsTransfer | ? { $_.JobState -ne "Transferred" }) {
:xfer while ((Get-BitsTransfer | ? { $_.JobState -eq "Transferring" }).Count -gt 0) {
$totalbytes=0;
$bytestransferred=0;
$timeTaken = 0;
foreach ($job in (Get-BitsTransfer | ? { $_.JobState -eq "Transferring" } | Sort-Object CreationTime)) {
$totalbytes += [math]::round($job.BytesTotal /1MB);
$totalSize = byteSize $($job.BytesTotal)
$bytestransferred += [math]::round($job.bytestransferred /1MB)
$transferSize = byteSize $($job.bytestransferred)
if ($timeTaken -eq 0) {
#Get the time of the oldest transfer aka the one that started first
$timeTaken = ((Get-Date) - $job.CreationTime).TotalMinutes
}
}
#TimeRemaining = (TotalFileSize - BytesDownloaded) * TimeElapsed/BytesDownloaded
[System.Windows.Forms.Application]::DoEvents()
# cancel download if asked
if ($script:cancelLoop -eq $true) {
cancelJob
# exit loop
break xfer
}
# pause download if asked
if ($script:pauseLoop -eq $true) {
$label_DLProgress.Text = "Download Paused!"
$label_DLProgress.ForeColor = "#ffff00" # yellow
Get-BitsTransfer | Suspend-BitsTransfer
# allow minimize while paused
$form.MinimizeBox = $true
# exit loop
break xfer
}
if ($totalbytes -gt 0 -and $bytestransferred -gt 0 -and $timetaken -gt 0) {
[int]$timeLeft = ($totalBytes - $bytestransferred) * ($timeTaken / $bytestransferred)
[int]$pctComplete = $(($bytestransferred*100)/$totalbytes);
$label_DLProgress.Text = "$transferSize of $totalSize ($pctComplete%) - Approx. $timeLeft minutes remaining"
$progressBar.Value = $pctComplete
#$label_DLProgress.Refresh()
Switch ($dlType)
{
'one' { $label_DLTitle.Text = "Downloading: $dlName"; Break }
'allEp' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Episodes $noLimitStatus"; Break }
'allTracks' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Tracks $noLimitStatus"; Break }
'allSeasons' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Episodes $noLimitStatus"; Break }
'allAlbums' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Tracks $noLimitStatus"; Break }
}
$progressBar.PerformStep()
Start-Sleep -Seconds 1
}
}
}
# download went too fast. Pause to show title downloaded
else {
Switch ($dlType)
{
'one' { $label_DLTitle.Text = "Downloading: $dlName"; Break }
'allEp' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Episodes"; Break }
'allTracks' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Tracks"; Break }
'allSeasons' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Episodes"; Break }
'allAlbums' { $label_DLTitle.Text = "Completed: $([int]$myjob.FilesTransferred) of $([int]$myjob.FilesTotal) Tracks"; Break }
}
Start-Sleep -Seconds 2
}
# Finish and close Bitstransfer
if ($script:cancelLoop -eq $false -and $script:pauseLoop -eq $false) {
if (Get-BitsTransfer | ? { $_.JobState -like "*Error*" }) {
$bitsError = (Get-BitsTransfer | select ErrorDescription).ErrorDescription
if ($debug) {
$eMSG = "$(Get-Date): Download Error. $bitsError"
$eMSG | Out-File ".\saverrLog.txt" -Append
}
$label_DLTitle.ForeColor = "#ff0000"
$label_DLProgress.Text = ""
$label_DLTitle.Text = "Error: $bitsError"
$progressBar.Visible = $false
$progressBar.Value = 0
$button_download.Enabled = $true
$button_cancel.Visible = $false
$button_cancel.Enabled = $false
$checkBoxButton_pause.Visible = $false
$CheckBoxButton_pause.Enabled = $false
}
else {
if ($startTime -ne $null) {
$howLong = (get-date).Subtract($startTime)
if ($howLong.Minutes -eq "0") {
$dlTime = "$($howLong.Seconds) seconds"
}
else {
$dlTime = "$($howLong.Minutes) minutes"
}
}
else {
$dlTime = "Unknown time"
}
$label_DLProgress.Text = ""
$label_DLTitle.ForeColor = "#00ff00"
$label_DLTitle.Text = "Download Completed in: $dlTime"
$button_download.Enabled = $true
$button_cancel.Visible = $false
$button_cancel.Enabled = $false
$checkBoxButton_pause.Visible = $false
$CheckBoxButton_pause.Enabled = $false
$progressBar.Visible = $false
$progressBar.Value = 0
}
Get-BitsTransfer | Complete-BitsTransfer
# remove any empty folder created
if ($dlType -like "all*") {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
# allow minimize again
$form.MinimizeBox = $true
}
}
}
Catch {
logit
Get-BitsTransfer | Remove-BitsTransfer
# clean up any empty folders
if ($dlType -like "all*") {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
$label_DLTitle.ForeColor = "#ff0000"
$label_DLProgress.Text = ""
$label_DLTitle.Text = "There was an error with the download."
$progressBar.Visible = $false
$progressBar.Value = 0
$button_download.Enabled = $true
$button_cancel.Visible = $false
$button_cancel.Enabled = $false
# enable minimize again
$form.MinimizeBox = $true
}
})
$button_search.Add_Click({search})
$button_settings.Add_Click({[void]$form2.ShowDialog()})
$button_cancel.Add_Click({
$cancelMsg = [System.Windows.Forms.MessageBox]::Show("Are you sure you want to cancel the download?`nAll non-completed files will be deleted!",'Cancel Download','YesNo','Question')
if ($cancelMsg -eq "Yes" -and $script:pauseLoop -ne $true) {
$script:cancelLoop = $true
}
elseif ($cancelMsg -eq "Yes" -and $script:pauseLoop -eq $true) {
$script:cancelLoop = $true
cancelJob
}
})
$button2_getToken.Add_Click({getToken})
$button2_servers.Add_Click({getServers})
$button2_dlPath.Add_Click({
try {
$script:dlPath = Get-SavePath
if ($dlPath) {
$textBox2_dlPath.Text = $dlPath
$label2_pathStatus.ForeColor = "#00ff00"
$label2_pathStatus.Text = "Path Saved!"
}
}
catch {
logit
$label2_pathStatus.ForeColor = "#ff0000"
$label2_pathStatus.Text = "Error! Check log"
}
})
# save token on enter key
$textBox2_password.Add_KeyUp({
if ($_.KeyCode -eq "Enter") {
getToken
}
})
# search on enter key
$textBox_search.Add_KeyUp({
if ($_.KeyCode -eq "Enter") {search}
})
$checked_type ={
if ($RadioButton_movie.Checked){
clearMediaInfo
clearDLStatus
$textBox_search.Text = ""
$label_seasons.Text = ""
$label_episodes.Text = ""
$label_search.text = "Search Movie:"
$combobox_seasons.Visible = $false
$combobox_episodes.Visible = $false
$label_mediaTitle.location = $label_mediaTitle_default_xy
$label_mediaRating.location = $label_mediaRating_default_xy
$label_mediaScore.location = $label_mediaScore_default_xy
$label_mediaSummary.location = $label_mediaSummary_default_xy
$label_mediaSummary.height = $label_mediaSummary_default_height
$toolTip.SetToolTip($label_search, "Searches by First Letter. Excluding 'The' and 'A'")
}
if ($RadioButton_tv.Checked){
clearMediaInfo
clearDLStatus
$textBox_search.Text = ""
$label_search.text = "Search TV Show:"
$label_seasons.Text = "Season:"
$label_episodes.text = "Episode:"
$combobox_seasons.Visible = $true
$combobox_episodes.Visible = $true
$label_episodes.location = New-Object System.Drawing.Point(215,225)
$comboBox_episodes.location = New-Object System.Drawing.Point(270,215)
$label_mediaTitle.location = $label_mediaTitle_default_xy
$label_mediaRating.location = $label_mediaRating_default_xy
$label_mediaScore.location = $label_mediaScore_default_xy
$label_mediaSummary.location = $label_mediaSummary_default_xy
$label_mediaSummary.height = $label_mediaSummary_default_height
$combobox_seasons.width = 130
$combobox_episodes.width = 45
$toolTip.SetToolTip($label_search, "Searches by first Letter. Excluding 'The' and 'A'")
}
elseif ($radiobutton_music.Checked) {
clearMediaInfo
clearDLStatus
$textBox_search.Text = ""
$label_search.text = "Search Artist:"
$label_seasons.Text = "Album:"
$label_episodes.Text = "Track:"
$label_episodes.location = New-Object System.Drawing.Point(280,225)
$comboBox_episodes.location = New-Object System.Drawing.Point(325,215)
$combobox_seasons.Visible = $true
$combobox_episodes.Visible = $true
$combobox_seasons.width = 195
$combobox_episodes.width = 210
$label_mediaTitle.location = New-Object System.Drawing.Point(140,280)
$label_mediaRating.location = New-Object System.Drawing.Point(140,300)
$label_mediaScore.location = New-Object System.Drawing.Point(140,300)
$label_mediaSummary.location = New-Object System.Drawing.Point(140,320)
$label_mediaSummary.height = 85
$toolTip.SetToolTip($label_search, "Searches by Artist Name")
}
}
$checkBoxButton_pause.Add_CheckedChanged({
if ($checkBoxButton_pause.Checked -eq $true){
$script:pauseLoop = $true
$checkBoxButton_pause.Text = "Resume"
}
else {
if ((Get-BitsTransfer).JobState -eq "Suspended") {
$script:pauseLoop = $false
$checkBoxButton_pause.Text = "Pause"
$button_download.Enabled = $true
$button_download.PerformClick()
}
}
})
$checkBox_debug.Add_CheckedChanged({
if ($checkBox_debug.Checked -eq $true){
$setDebug = $true
}
else {
$setDebug = $false
}
# update settings file
if (Test-Path .\saverrSettings.xml) {
$script:settings = Import-Clixml .\saverrSettings.xml
Add-Member -InputObject $settings -MemberType NoteProperty -Name 'logging' -Value $setDebug -force
$settings | Export-Clixml .\saverrSettings.xml
}
else {
$script:settings = [pscustomobject] @{
logging = $setDebug
}
$settings | Export-Clixml .\saverrSettings.xml
}
$settings = Import-Clixml .\saverrSettings.xml
$debug = $settings.logging
})
$checkBox_ssl.Add_CheckedChanged({
if ($checkBox_ssl.Checked -eq $true){
$setSSL = $true
}
else {
$setSSL = $false
}
# update settings file
if (Test-Path .\saverrSettings.xml) {
$script:settings = Import-Clixml .\saverrSettings.xml
Add-Member -InputObject $settings -MemberType NoteProperty -Name 'ssl' -Value $setSSL -force
$settings | Export-Clixml .\saverrSettings.xml
}
else {
$script:settings = [pscustomobject] @{
ssl = $setSSL
}
$settings | Export-Clixml .\saverrSettings.xml
}
$settings = Import-Clixml .\saverrSettings.xml
$ssl = $settings.ssl
})
# show extra season/artist fields
$RadioButton_movie.Add_CheckedChanged($checked_type)
$RadioButton_tv.Add_CheckedChanged($checked_type)
$RadioButton_music.Add_CheckedChanged($checked_type)
# Show season media info on selection
$comboBox_results.Add_SelectedIndexChanged({mediaInfo})
# show episodes after season selection
$comboBox_seasons.Add_SelectedIndexChanged({episodeSelection})
# show ep media info after season selection
$comboBox_episodes.Add_SelectedIndexChanged({mediaEpInfo})
# save server on selection
$comboBox2_servers.Add_SelectedValueChanged({saveServer})
$form2.add_FormClosing({clearStatusSave})
# confirm closing. clear any downloads on close.
$form.add_FormClosing({
if (Get-BitsTransfer) {
$question = [System.Windows.Forms.MessageBox]::Show("Are you sure you want to Exit?`nAll non-completed files will be deleted!", 'Exit Saverr', 'YesNo', 'Question')
if ($question -eq 'Yes') {
$script:cancelLoop = $true
Get-BitsTransfer | Complete-BitsTransfer
# remove empty folders created
if (($dlType -like "all*") -and ($script:pauseLoop -eq $true)) {
if (Test-Path $allSeasonPath) {
Get-ChildItem $allSeasonPath -Directory -recurse | where {-NOT $_.GetFiles("*","AllDirectories")} | del -recurse -ErrorAction SilentlyContinue
if (Test-Path $allSeasonPath) {
if ((Get-ChildItem $allSeasonPath | Measure-Object).Count -eq 0) {
del $allSeasonPath -ErrorAction SilentlyContinue
}
}
}
}
}
else {
$_.Cancel = $true
}
}
})
# show the form
[void]$form.ShowDialog()
# close the forms
$form.Dispose()
$form2.Dispose()
# end
================================================
FILE: screenshots/README.MD
================================================
## Screenshots
gitextract_tny7vz03/
├── LICENSE
├── README.md
├── Saverr.lnk
├── Saverr.ps1
└── screenshots/
└── README.MD
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (119K chars).
[
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2019 ninthwalker\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 6564,
"preview": "# Saverr\n\n## Developers Note:\nThis script/repo is no longer maintained. \nWhile it still works as far as i know, I'm not"
},
{
"path": "Saverr.ps1",
"chars": 105495,
"preview": "#######################################\r\n# Name: Saverr #\r\n# Desc: d/l media from Plex "
},
{
"path": "screenshots/README.MD",
"chars": 15,
"preview": "## Screenshots\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the ninthwalker/saverr GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (110.5 KB), approximately 33.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.