├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── Arguments.cs ├── LICENSE ├── LocalSystem ├── Msi │ ├── Api.cs │ ├── InstallProperty.cs │ ├── InstalledProduct.cs │ └── MsiException.cs ├── WinApi.cs ├── WindowsUser.cs └── WindowsUserSidNotFound.cs ├── Log.cs ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Plex ├── Api.cs ├── AppNotInstalledException.cs ├── EventSource.cs ├── MediaContainer.cs ├── MediaServer.cs ├── PlexDataFolderNotFoundException.cs ├── Registry.cs ├── ServerService.cs ├── ServiceNotInstalledException.cs ├── SilentUpdate.cs └── Update │ ├── CurrentVersion.cs │ ├── Package.cs │ ├── Release.cs │ └── SystemType.cs ├── PlexServerAutoUpdater.csproj ├── PlexServerAutoUpdater.sln ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── SystemExitCodes.cs └── app.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: TechieGuy12 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | *.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | paket-files/ 247 | 248 | # FAKE - F# Make 249 | .fake/ 250 | 251 | # JetBrains Rider 252 | .idea/ 253 | *.sln.iml 254 | 255 | # CodeRush 256 | .cr/ -------------------------------------------------------------------------------- /Arguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace TE 6 | { 7 | /// 8 | /// Description of Arguments. 9 | /// 10 | /// 11 | /// Arguments class 12 | /// 13 | public class Arguments 14 | { 15 | // Variables 16 | private StringDictionary Parameters; 17 | 18 | // Constructor 19 | public Arguments(string[] Args) 20 | { 21 | Parameters = new StringDictionary(); 22 | Regex Spliter = new Regex(@"^-{1,2}|^/|=|:(?!\\)", 23 | RegexOptions.IgnoreCase | RegexOptions.Compiled); 24 | 25 | Regex Remover = new Regex(@"^['""]?(.*?)['""]?$", 26 | RegexOptions.IgnoreCase | RegexOptions.Compiled); 27 | 28 | string Parameter = null; 29 | string[] Parts; 30 | 31 | // Valid parameters forms: 32 | // {-,/,--}param{ ,=,:}((",')value(",')) 33 | // Examples: 34 | // -param1 value1 --param2 /param3:"Test-:-work" 35 | // /param4=happy -param5 '--=nice=--' 36 | foreach (string Txt in Args) 37 | { 38 | // Look for new parameters (-,/ or --) and a 39 | // possible enclosed value (=,:) 40 | Parts = Spliter.Split(Txt, 3); 41 | 42 | switch (Parts.Length) 43 | { 44 | // Found a value (for the last parameter 45 | // found (space separator)) 46 | case 1: 47 | if (Parameter != null) 48 | { 49 | if (!Parameters.ContainsKey(Parameter)) 50 | { 51 | Parts[0] = 52 | Remover.Replace(Parts[0], "$1"); 53 | 54 | Parameters.Add(Parameter, Parts[0]); 55 | } 56 | Parameter = null; 57 | } 58 | // else Error: no parameter waiting for a value (skipped) 59 | break; 60 | 61 | // Found just a parameter 62 | case 2: 63 | // The last parameter is still waiting. 64 | // With no value, set it to true. 65 | if (Parameter != null) 66 | { 67 | if (!Parameters.ContainsKey(Parameter)) 68 | Parameters.Add(Parameter, "true"); 69 | } 70 | Parameter = Parts[1]; 71 | break; 72 | 73 | // Parameter with enclosed value 74 | case 3: 75 | // The last parameter is still waiting. 76 | // With no value, set it to true. 77 | if (Parameter != null) 78 | { 79 | if (!Parameters.ContainsKey(Parameter)) 80 | Parameters.Add(Parameter, "true"); 81 | } 82 | 83 | Parameter = Parts[1]; 84 | 85 | // Remove possible enclosing characters (",') 86 | if (!Parameters.ContainsKey(Parameter)) 87 | { 88 | Parts[2] = Remover.Replace(Parts[2], "$1"); 89 | Parameters.Add(Parameter, Parts[2]); 90 | } 91 | 92 | Parameter = null; 93 | break; 94 | } 95 | } 96 | // In case a parameter is still waiting 97 | if (Parameter != null) 98 | { 99 | if (!Parameters.ContainsKey(Parameter)) 100 | Parameters.Add(Parameter, "true"); 101 | } 102 | } 103 | 104 | // Retrieve a parameter value if it exists 105 | // (overriding C# indexer property) 106 | public string this[string Param] 107 | { 108 | get 109 | { 110 | return (Parameters[Param]); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Paul Salmon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LocalSystem/Msi/Api.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace TE.LocalSystem.Msi 7 | { 8 | #region Enumerations 9 | /// 10 | /// All avalible MSI Setup install exit codes. 11 | /// 12 | public enum MsiExitCodes 13 | { 14 | /// 15 | /// Action completed successfully. 16 | /// 17 | /// 18 | /// ERROR_SUCCESS 19 | /// 20 | Success = 0, 21 | 22 | /// 23 | /// The data is invalid. 24 | /// 25 | /// 26 | /// ERROR_INVALID_DATA 27 | /// 28 | InvalidDataError = 13, 29 | 30 | /// 31 | /// One of the parameters was invalid. 32 | /// 33 | /// 34 | /// ERROR_INVALID_PARAMETER 35 | /// 36 | InvalidParameterError = 87, 37 | 38 | /// 39 | /// This function is not available for this platform. 40 | /// It is only available on Windows 2000 and 41 | /// Windows XP with Window Installer version 2.0. 42 | /// 43 | /// 44 | /// ERROR_CALL_NOT_IMPLEMENTED 45 | /// 46 | CallNotImplementedError = 120, 47 | 48 | 49 | MoreData = 234, 50 | 51 | /// 52 | /// This error happens when there is no more items available for enumeration 53 | /// 54 | NoMoreItems = 259, 55 | 56 | 57 | /// 58 | /// This error code only occurs when using 59 | /// Windows Installer version 2.0 and Windows XP or later. 60 | /// If Windows Installer determines a product may be incompatible 61 | /// with the current operating system, 62 | /// it displays a dialog informing the user and asking whether to try to install anyway. 63 | /// This error code is returned if the user chooses not to try the installation. 64 | /// 65 | /// 66 | /// ERROR_APPHELP_BLOCK 67 | /// 68 | ApplicationHelpBlockedError = 1259, 69 | 70 | /// 71 | /// The Windows Installer service could not be accessed. 72 | /// Contact your support personnel to verify that the 73 | /// Windows Installer service is properly registered. 74 | /// 75 | /// 76 | /// ERROR_INSTALL_SERVICE_FAILURE 77 | /// 78 | InstallServiceFailureError = 1601, 79 | 80 | /// 81 | /// User cancel installation. 82 | /// 83 | /// 84 | /// ERROR_INSTALL_USEREXIT 85 | /// 86 | UserExitError = 1602, 87 | 88 | /// 89 | /// Fatal error during installation. 90 | /// 91 | /// 92 | /// ERROR_INSTALL_FAILURE 93 | /// 94 | FatalInstallFailureError = 1603, 95 | 96 | /// 97 | /// Installation suspended, incomplete. 98 | /// 99 | /// 100 | /// ERROR_INSTALL_SUSPEND 101 | /// 102 | InstallSuspendedError = 1604, 103 | 104 | /// 105 | /// This action is only valid for products that are currently installed. 106 | /// 107 | /// 108 | /// ERROR_UNKNOWN_PRODUCT 109 | /// 110 | UnknownProductError = 1605, 111 | 112 | /// 113 | /// Feature ID not registered. 114 | /// 115 | /// 116 | /// ERROR_UNKNOWN_FEATURE 117 | /// 118 | UnknownFeatureError = 1606, 119 | 120 | /// 121 | /// Component ID not registered. 122 | /// 123 | /// 124 | /// ERROR_UNKNOWN_COMPONENT 125 | /// 126 | UnknownComponentError = 1607, 127 | 128 | /// 129 | /// Unknown property. 130 | /// 131 | /// 132 | /// ERROR_UNKNOWN_PROPERTY 133 | /// 134 | UnknownPropertyError = 1608, 135 | 136 | /// 137 | /// Handle is in an invalid state. 138 | /// 139 | /// 140 | /// ERROR_INVALID_HANDLE_STATE 141 | /// 142 | InvalidHandleStateError = 1609, 143 | 144 | /// 145 | /// The configuration data for this product is corrupt. 146 | /// Contact your support personnel. 147 | /// 148 | /// 149 | /// ERROR_BAD_CONFIGURATION 150 | /// 151 | BadConfigurationError = 1610, 152 | 153 | /// 154 | /// Component qualifier not present. 155 | /// 156 | /// 157 | /// ERROR_INDEX_ABSENT 158 | /// 159 | IndexAbsentError = 1611, 160 | 161 | /// 162 | /// The installation source for this product is not available. 163 | /// Verify that the source exists and that you can access it. 164 | /// 165 | /// 166 | /// ERROR_INSTALL_SOURCE_ABSENT 167 | /// 168 | InstallSourceAbsentError = 1612, 169 | 170 | /// 171 | /// This installation package cannot be installed by the Windows Installer service. 172 | /// You must install a Windows service pack that contains 173 | /// a newer version of the Windows Installer service. 174 | /// 175 | /// 176 | /// ERROR_INSTALL_PACKAGE_VERSION 177 | /// 178 | WrongInstallPackageVersionError = 1613, 179 | 180 | /// 181 | /// Product is uninstalled. 182 | /// 183 | /// 184 | /// ERROR_PRODUCT_UNINSTALLED 185 | /// 186 | ProductUninstalledError = 1614, 187 | 188 | /// 189 | /// SQL query syntax invalid or unsupported. 190 | /// 191 | /// 192 | /// ERROR_BAD_QUERY_SYNTAX 193 | /// 194 | BadQuerySyntaxError = 1615, 195 | 196 | /// 197 | /// Record field does not exist. 198 | /// 199 | /// 200 | /// ERROR_INVALID_FIELD 201 | /// 202 | InvalidFieldError = 1616, 203 | 204 | /// 205 | /// Another installation is already in progress. 206 | /// Complete that installation before proceeding with this install. 207 | /// 208 | /// 209 | /// ERROR_INSTALL_ALREADY_RUNNING 210 | /// 211 | InstallInProgressError = 1618, 212 | 213 | /// 214 | /// This installation package could not be opened. 215 | /// Verify that the package exists and that you can access it, 216 | /// or contact the application vendor to verify that 217 | /// this is a valid Windows Installer package. 218 | /// 219 | /// 220 | /// ERROR_INSTALL_PACKAGE_OPEN_FAILED 221 | /// 222 | InstallPackageOpenError = 1619, 223 | 224 | /// 225 | /// This installation package could not be opened. 226 | /// Contact the application vendor to verify that 227 | /// this is a valid Windows Installer package. 228 | /// 229 | /// 230 | /// ERROR_INSTALL_PACKAGE_INVALID 231 | /// 232 | InstallPackageInvalidError = 1620, 233 | 234 | /// 235 | /// There was an error starting the Windows Installer service user interface. 236 | /// Contact your support personnel. 237 | /// 238 | /// 239 | /// ERROR_INSTALL_UI_FAILURE 240 | /// 241 | InstallUIError = 1621, 242 | 243 | /// 244 | /// Error opening installation log file. 245 | /// Verify that the specified log file location exists and is writable. 246 | /// 247 | /// 248 | /// ERROR_INSTALL_LOG_FAILURE 249 | /// 250 | InstallLogError = 1622, 251 | 252 | /// 253 | /// This language of this installation package is not supported by your system. 254 | /// 255 | /// 256 | /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED 257 | /// 258 | InstallLanguageUnsupportedError = 1623, 259 | 260 | /// 261 | /// Error applying transforms. 262 | /// Verify that the specified transform paths are valid. 263 | /// 264 | /// 265 | /// ERROR_INSTALL_TRANSFORM_FAILURE 266 | /// 267 | InstallTransformError = 1624, 268 | 269 | /// 270 | /// This installation is forbidden by system policy. 271 | /// Contact your system administrator. 272 | /// 273 | /// 274 | /// ERROR_INSTALL_PACKAGE_REJECTED 275 | /// 276 | InstallPackageRejectedError = 1625, 277 | 278 | /// 279 | /// Function could not be executed. 280 | /// 281 | /// 282 | /// ERROR_FUNCTION_NOT_CALLED 283 | /// 284 | FunctionNotCalledError = 1626, 285 | 286 | /// 287 | /// Function failed during execution. 288 | /// 289 | /// 290 | /// ERROR_FUNCTION_FAILED 291 | /// 292 | FunctionFailedError = 1627, 293 | 294 | /// 295 | /// Invalid or unknown table specified. 296 | /// 297 | /// 298 | /// ERROR_INVALID_TABLE 299 | /// 300 | InvalidTableError = 1628, 301 | 302 | /// 303 | /// Data supplied is of wrong type. 304 | /// 305 | /// 306 | /// ERROR_DATATYPE_MISMATCH 307 | /// 308 | DatatypeMismatchError = 1629, 309 | 310 | /// 311 | /// Data of this type is not supported. 312 | /// 313 | /// 314 | /// ERROR_UNSUPPORTED_TYPE 315 | /// 316 | UnsupportedTypeError = 1630, 317 | 318 | /// 319 | /// The Windows Installer service failed to start. 320 | /// Contact your support personnel. 321 | /// 322 | /// 323 | /// ERROR_CREATE_FAILED 324 | /// 325 | CreateFailedError = 1631, 326 | 327 | /// 328 | /// The temp folder is either full or inaccessible. 329 | /// Verify that the temp folder exists and that you can write to it. 330 | /// 331 | /// 332 | /// ERROR_INSTALL_TEMP_UNWRITABLE 333 | /// 334 | InstallTempUnwritableError = 1632, 335 | 336 | /// 337 | /// This installation package is not supported on this platform. 338 | /// Contact your application vendor. 339 | /// 340 | /// 341 | /// ERROR_INSTALL_PLATFORM_UNSUPPORTED 342 | /// 343 | InstallPlatformUnsupportedError = 1633, 344 | 345 | /// 346 | /// Component not used on this machine 347 | /// 348 | /// 349 | /// ERROR_INSTALL_NOTUSED 350 | /// 351 | InstallNotusedError = 1634, 352 | 353 | /// 354 | /// This patch package could not be opened. 355 | /// Verify that the patch package exists and that you can access it, 356 | /// or contact the application vendor to verify that 357 | /// this is a valid Windows Installer patch package. 358 | /// 359 | /// 360 | /// ERROR_PATCH_PACKAGE_OPEN_FAILED 361 | /// 362 | PatchPackageOpenFailedError = 1635, 363 | 364 | /// 365 | /// This patch package could not be opened. 366 | /// Contact the application vendor to verify that 367 | /// this is a valid Windows Installer patch package. 368 | /// 369 | /// 370 | /// ERROR_PATCH_PACKAGE_INVALID 371 | /// 372 | PatchPackageInvalidError = 1636, 373 | 374 | /// 375 | /// This patch package cannot be processed by the Windows Installer service. 376 | /// You must install a Windows service pack that contains 377 | /// a newer version of the Windows Installer service. 378 | /// 379 | /// 380 | /// ERROR_PATCH_PACKAGE_UNSUPPORTED 381 | /// 382 | PatchPackageUnsupportedError = 1637, 383 | 384 | /// 385 | /// Another version of this product is already installed. 386 | /// Installation of this version cannot continue. 387 | /// To configure or remove the existing version of this product, 388 | /// use Add/Remove Programs on the Control Panel. 389 | /// 390 | /// 391 | /// ERROR_PRODUCT_VERSION 392 | /// 393 | ProductVersionError = 1638, 394 | 395 | /// 396 | /// Invalid command line argument. 397 | /// Consult the Windows Installer SDK for detailed command line help. 398 | /// 399 | /// 400 | /// ERROR_INVALID_COMMAND_LINE 401 | /// 402 | InvalidCommandLineError = 1639, 403 | 404 | /// 405 | /// Installation from a Terminal Server client session not permitted for current user. 406 | /// 407 | /// 408 | /// ERROR_INSTALL_REMOTE_DISALLOWED 409 | /// 410 | RemoteInstallDisallowedError = 1640, 411 | 412 | /// 413 | /// The installer has started a reboot. 414 | /// This error code not available on Windows Installer version 1.0. 415 | /// 416 | /// 417 | /// ERROR_SUCCESS_REBOOT_INITIATED 418 | /// 419 | RebootSuccessInitiatedError = 1641, 420 | 421 | /// 422 | /// The installer cannot install the upgrade patch because 423 | /// the program being upgraded may be missing or the upgrade 424 | /// patch updates a different version of the program. 425 | /// Verify that the program to be upgraded exists on your 426 | /// computer and that you have the correct upgrade patch. 427 | /// 428 | /// 429 | /// ERROR_PATCH_TARGET_NOT_FOUND 430 | /// 431 | PatchTargetNotFoundError = 1642, 432 | 433 | /// 434 | /// The patch package is not permitted by system policy. 435 | /// This error code is available with Windows Installer versions 2.0 or later. 436 | /// 437 | /// 438 | /// ERROR_PATCH_PACKAGE_REJECTED 439 | /// 440 | PatchPackageRejectedError = 1643, 441 | 442 | /// 443 | /// One or more customizations are not permitted by system policy. 444 | /// This error code is available with Windows Installer versions 2.0 or later. 445 | /// 446 | /// 447 | /// ERROR_INSTALL_TRANSFORM_REJECTED 448 | /// 449 | InstallTransformRejectedError = 1644, 450 | 451 | /// 452 | /// A reboot is required to complete the install. 453 | /// This does not include installs where the ForceReboot action is run. 454 | /// This error code not available on Windows Installer version 1.0. 455 | /// 456 | /// 457 | /// ERROR_SUCCESS_REBOOT_REQUIRED 458 | /// 459 | RebootRequiredSuccessError = 3010 460 | 461 | } 462 | 463 | /// 464 | /// Install context of a product. 465 | /// 466 | public enum InstallContext : int 467 | { 468 | Node = 0, 469 | UserManaged = 1, 470 | UserUnmanaged = 2, 471 | Machine = 4, 472 | All = (UserManaged | UserUnmanaged | Machine), 473 | } 474 | #endregion 475 | 476 | /// 477 | /// Wrapper for the Windows Installer API. 478 | /// 479 | public static class Api 480 | { 481 | #region API Declarations 482 | [DllImport("msi.dll", CharSet = CharSet.Unicode)] 483 | private static extern Int32 MsiGetProductInfo( 484 | string product, 485 | string property, 486 | [Out] String valueBuf, 487 | ref Int32 len); 488 | 489 | [DllImport("msi.dll", 490 | EntryPoint = "MsiEnumProductsExW", 491 | CharSet = CharSet.Unicode, 492 | ExactSpelling = true, 493 | CallingConvention = CallingConvention.StdCall)] 494 | private static extern uint MsiEnumProductsEx( 495 | string szProductCode, 496 | string szUserSid, 497 | uint dwContext, 498 | uint dwIndex, 499 | string szInstalledProductCode, 500 | out object pdwInstalledProductContext, 501 | string szSid, 502 | ref uint pccSid); 503 | 504 | [DllImport("msi.dll", CharSet = CharSet.Unicode)] 505 | private static extern uint MsiEnumComponents( 506 | uint iComponentIndex, 507 | StringBuilder lpComponentBuf); 508 | 509 | [DllImport("msi.dll", CharSet = CharSet.Unicode)] 510 | private static extern UInt32 MsiLocateComponent( 511 | string szComponent, 512 | [Out] StringBuilder lpPathBuf, 513 | ref UInt32 pcchBuf); 514 | #endregion 515 | 516 | #region Public Functions 517 | /// 518 | /// Enumerate all installed components. 519 | /// 520 | /// A List of strings containing all component GUIDs 521 | public static List EnumerateComponents() 522 | { 523 | List guidList = new List(); 524 | uint ret = 0; 525 | uint i = 0; 526 | 527 | do 528 | { 529 | // Create the guid buffer 530 | StringBuilder guid = new StringBuilder(39); 531 | 532 | // Get a component GUID 533 | ret = Api.MsiEnumComponents(i, guid); 534 | 535 | // If the return code indicates a success, then add the GUID 536 | // to the list 537 | if (ret == 0) 538 | { 539 | guidList.Add(guid.ToString()); 540 | } 541 | 542 | // Increment the counter 543 | i++; 544 | 545 | } while (ret != (uint)MsiExitCodes.NoMoreItems); 546 | 547 | return guidList; 548 | } 549 | 550 | /// 551 | /// Enumerate all installed products. 552 | /// 553 | /// A List of strings containing all GUIDs 554 | public static List EnumerateProducts() 555 | { 556 | var guidList = new List(); 557 | uint ret = 0, i = 0, dummy2 = 0; 558 | do 559 | { 560 | string guid = new string(new char[39]); 561 | object dummy1; 562 | ret = Api.MsiEnumProductsEx(null, null, (uint)InstallContext.All, i, guid, out dummy1, null, ref dummy2); 563 | if (ret == 0) 564 | { 565 | guidList.Add(guid); 566 | } 567 | i++; 568 | } while (ret != (uint)MsiExitCodes.NoMoreItems); 569 | 570 | return guidList; 571 | } 572 | 573 | /// 574 | /// Gets the path of the component using a GUID. 575 | /// 576 | /// 577 | /// The GUID of the component. 578 | /// 579 | /// 580 | /// The full path of the component. 581 | /// 582 | public static string GetComponentPath(string componentGuid) 583 | { 584 | //string path = new string(new char[255]); 585 | UInt32 buffer = 500; 586 | StringBuilder path = new StringBuilder(255); 587 | 588 | MsiLocateComponent( 589 | componentGuid, 590 | path, 591 | ref buffer); 592 | 593 | return path.ToString(); 594 | } 595 | 596 | /// 597 | /// Gets the path of a component using the key file of the component. 598 | /// 599 | /// 600 | /// The name of the file. 601 | /// 602 | /// 603 | /// The full path of the component of an empty string if the path was 604 | /// not found. 605 | /// 606 | public static string GetComponentPathByFile(string fileName) 607 | { 608 | string path = string.Empty; 609 | uint ret = 0; 610 | uint i = 0; 611 | 612 | do 613 | { 614 | // Create the guid buffer 615 | StringBuilder guid = new StringBuilder(39); 616 | 617 | // Get a component GUID 618 | ret = Api.MsiEnumComponents(i, guid); 619 | 620 | // If the return code indicates a success, then add the GUID 621 | // to the list 622 | if (ret == 0) 623 | { 624 | string componentPath = 625 | GetComponentPath(guid.ToString()); 626 | if (componentPath.Contains(fileName)) 627 | { 628 | path = componentPath; 629 | } 630 | } 631 | 632 | // Increment the counter 633 | i++; 634 | 635 | } while ((ret != (uint)MsiExitCodes.NoMoreItems) && (path.Length == 0)); 636 | 637 | return path; 638 | } 639 | 640 | /// 641 | /// Get property of a product indicated by GUID. Throws exception if cannot read the property. 642 | /// 643 | /// Product GUID 644 | /// Property name 645 | /// Property value, if available. 646 | /// Throws MSIException if reading property was not successful 647 | public static String getProperty(string productGUID, string propertyName) 648 | { 649 | int len = 0; 650 | // Get the data len 651 | Api.MsiGetProductInfo(productGUID, propertyName, null, ref len); 652 | // increase for the terminating \0 653 | len++; 654 | 655 | String r = new string(new char[len]); 656 | int returnValue = Api.MsiGetProductInfo(productGUID, propertyName, r, ref len); 657 | if (returnValue != 0) 658 | { 659 | if (propertyName != InstallProperty.DisplayName) 660 | { 661 | throw new MSIException(returnValue); 662 | } 663 | } 664 | return r; 665 | } 666 | #endregion 667 | } 668 | } 669 | -------------------------------------------------------------------------------- /LocalSystem/Msi/InstallProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TE.LocalSystem.Msi 4 | { 5 | /// 6 | /// The standard properties for a Windows Installer file. 7 | /// 8 | public static class InstallProperty 9 | { 10 | public static readonly string InstalledProductName = "InstalledProductName"; 11 | public static readonly string VersionString = "VersionString"; 12 | public static readonly string HelpLink = "HelpLink"; 13 | public static readonly string HelpTelephone = "HelpTelephone"; 14 | public static readonly string InstallLocation = "InstallLocation"; 15 | public static readonly string InstallSource = "InstallSource"; 16 | public static readonly string InstallDate = "InstallDate"; 17 | public static readonly string Publisher = "Publisher"; 18 | public static readonly string LocalPackage = "LocalPackage"; 19 | public static readonly string URLInfoAbout = "URLInfoAbout"; 20 | public static readonly string URLUpdateInfo = "URLUpdateInfo"; 21 | public static readonly string VersionMinor = "VersionMinor"; 22 | public static readonly string VersionMajor = "VersionMajor"; 23 | public static readonly string ProductID = "ProductID"; 24 | public static readonly string RegCompany = "RegCompany"; 25 | public static readonly string RegOwner = "RegOwner"; 26 | public static readonly string Uninstallable = "Uninstallable"; 27 | public static readonly string State = "State"; 28 | public static readonly string PatchType = "PatchType"; 29 | public static readonly string LUAEnabled = "LUAEnabled"; 30 | public static readonly string DisplayName = "DisplayName"; 31 | public static readonly string MoreInfoURL = "MoreInfoURL"; 32 | public static readonly string LastUsedSource = "LastUsedSource"; 33 | public static readonly string LastUsedType = "LastUsedType"; 34 | public static readonly string MediaPackagePath = "MediaPackagePath"; 35 | public static readonly string DiskPrompt = "DiskPrompt"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LocalSystem/Msi/InstalledProduct.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using static System.Environment; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace TE.LocalSystem.Msi 7 | { 8 | /// 9 | /// Description of InstalledProduct. 10 | /// 11 | public class InstalledProduct 12 | { 13 | #region Public Variables 14 | public readonly string Guid; 15 | #endregion 16 | 17 | #region Properties 18 | public string InstalledProductName { get { return Api.getProperty(Guid, InstallProperty.InstalledProductName); } } 19 | public string VersionString { get { return Api.getProperty(Guid, InstallProperty.VersionString); } } 20 | public string HelpLink { get { return Api.getProperty(Guid, InstallProperty.HelpLink); } } 21 | public string HelpTelephone { get { return Api.getProperty(Guid, InstallProperty.HelpTelephone); } } 22 | public string InstallLocation { get { return Api.getProperty(Guid, InstallProperty.InstallLocation); } } 23 | public string InstallSource { get { return Api.getProperty(Guid, InstallProperty.InstallSource); } } 24 | public string InstallDate { get { return Api.getProperty(Guid, InstallProperty.InstallDate); } } 25 | public string Publisher { get { return Api.getProperty(Guid, InstallProperty.Publisher); } } 26 | public string LocalPackage { get { return Api.getProperty(Guid, InstallProperty.LocalPackage); } } 27 | public string URLInfoAbout { get { return Api.getProperty(Guid, InstallProperty.URLInfoAbout); } } 28 | public string URLUpdateInfo { get { return Api.getProperty(Guid, InstallProperty.URLUpdateInfo); } } 29 | public string VersionMinor { get { return Api.getProperty(Guid, InstallProperty.VersionMinor); } } 30 | public string VersionMajor { get { return Api.getProperty(Guid, InstallProperty.VersionMajor); } } 31 | public string ProductID { get { return Api.getProperty(Guid, InstallProperty.ProductID); } } 32 | public string RegCompany { get { return Api.getProperty(Guid, InstallProperty.RegCompany); } } 33 | public string RegOwner { get { return Api.getProperty(Guid, InstallProperty.RegOwner); } } 34 | public string Uninstallable { get { return Api.getProperty(Guid, InstallProperty.Uninstallable); } } 35 | public string State { get { return Api.getProperty(Guid, InstallProperty.State); } } 36 | public string PatchType { get { return Api.getProperty(Guid, InstallProperty.PatchType); } } 37 | public string LUAEnabled { get { return Api.getProperty(Guid, InstallProperty.LUAEnabled); } } 38 | public string DisplayName { get { return Api.getProperty(Guid, InstallProperty.DisplayName); } } 39 | public string MoreInfoURL { get { return Api.getProperty(Guid, InstallProperty.MoreInfoURL); } } 40 | public string LastUsedSource { get { return Api.getProperty(Guid, InstallProperty.LastUsedSource); } } 41 | public string LastUsedType { get { return Api.getProperty(Guid, InstallProperty.LastUsedType); } } 42 | public string MediaPackagePath { get { return Api.getProperty(Guid, InstallProperty.MediaPackagePath); } } 43 | public string DiskPrompt { get { return Api.getProperty(Guid, InstallProperty.DiskPrompt); } } 44 | #endregion 45 | 46 | #region Constructors 47 | public InstalledProduct(string guid) 48 | { 49 | this.Guid = guid; 50 | } 51 | #endregion 52 | 53 | #region Public Functions 54 | /// 55 | /// Enumerates all MSI installed products 56 | /// 57 | /// 58 | /// An enumeration containing InstalledProducts 59 | /// 60 | public static IEnumerable Enumerate() 61 | { 62 | foreach (var guid in Api.EnumerateProducts()) 63 | yield return new InstalledProduct(guid); 64 | } 65 | 66 | /// 67 | /// Returns a string that contains all the property names and values. 68 | /// 69 | /// 70 | /// A string that contains all the property names and values. 71 | /// 72 | public override string ToString() 73 | { 74 | StringBuilder sb = new StringBuilder(); 75 | foreach (var p in GetType().GetProperties()) 76 | { 77 | try 78 | { 79 | sb.AppendFormat($"{p.Name}:{p.GetValue(this)}{NewLine}"); 80 | } 81 | catch 82 | { } 83 | } 84 | return sb.ToString(); 85 | } 86 | #endregion 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /LocalSystem/Msi/MsiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace TE.LocalSystem.Msi 5 | { 6 | /// 7 | /// An issue occurred with the Windows Installer. 8 | /// 9 | [Serializable] 10 | internal class MSIException : Exception 11 | { 12 | /// 13 | /// Gets or sets the return value. 14 | /// 15 | public int ReturnValue { get; private set; } 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public MSIException() { } 21 | 22 | /// 23 | /// Initializes a new instance of the class 24 | /// when provided with the execption message. 25 | /// 26 | /// 27 | /// The exception message. 28 | /// 29 | public MSIException(string message) : base(message) { } 30 | 31 | /// 32 | /// Initializes a new instance of the class 33 | /// when provided with the return value. 34 | /// 35 | /// 36 | /// The return value. 37 | /// 38 | public MSIException(int returnValue) 39 | : this($"MSIError : {((MsiExitCodes)returnValue).ToString()}") 40 | { 41 | ReturnValue = returnValue; 42 | } 43 | 44 | /// 45 | /// Initializes a new instance of the class 46 | /// when provided with the execption message and inner exception. 47 | /// 48 | /// 49 | /// The exception message. 50 | /// 51 | /// 52 | /// The inner exception. 53 | /// 54 | public MSIException(string message, Exception innerException) 55 | : base(message, innerException) { } 56 | 57 | /// 58 | /// Initializes a new instance of the class 59 | /// when provided with the serialization information and streaming 60 | /// context. 61 | /// 62 | /// 63 | /// The serialization information. 64 | /// 65 | /// 66 | /// The streaming context. 67 | /// 68 | protected MSIException( 69 | SerializationInfo info, 70 | StreamingContext context) 71 | : base(info, context) { } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LocalSystem/WinApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace TE.LocalSystem 6 | { 7 | /// 8 | /// API and constant definitions for using the Windows API. 9 | /// 10 | public static class WinApi 11 | { 12 | #region Public Constants 13 | /// 14 | /// No error. 15 | /// 16 | public const int NO_ERROR = 0; 17 | 18 | /// 19 | /// The buffer isn't sufficient to store the result. 20 | /// 21 | public const int ERROR_INSUFFICIENT_BUFFER = 122; 22 | 23 | /// 24 | /// Invalid flags. 25 | /// 26 | /// 27 | /// On Windows Server 2003 this error is/can be returned, but processing can still continue. 28 | /// 29 | public const int ERROR_INVALID_FLAGS = 1004; 30 | #endregion 31 | 32 | #region Public Enumerations 33 | /// 34 | /// contains values that specify the type of a security identifier (SID). 35 | /// 36 | public enum SID_NAME_USE 37 | { 38 | /// 39 | /// A user SID. 40 | /// 41 | SidTypeUser = 1, 42 | /// 43 | /// A group SID. 44 | /// 45 | SidTypeGroup, 46 | /// 47 | /// A domain SID. 48 | /// 49 | SidTypeDomain, 50 | /// 51 | /// An alias SID. 52 | /// 53 | SidTypeAlias, 54 | /// 55 | /// A SID for a well-known group. 56 | /// 57 | SidTypeWellKnownGroup, 58 | /// 59 | /// A SID for a deleted account. 60 | /// 61 | SidTypeDeletedAccount, 62 | /// 63 | /// A SID That is not valid. 64 | /// 65 | SidTypeInvalid, 66 | /// 67 | /// A SID of unknown type. 68 | /// 69 | SidTypeUnknown, 70 | /// 71 | /// A SID for a computer. 72 | /// 73 | SidTypeComputer, 74 | /// 75 | /// A mandatory integrity label SID. 76 | /// 77 | SidTypeLabel 78 | } 79 | #endregion 80 | 81 | #region Public API Functions 82 | /// 83 | /// The LookupAccountName function accepts the name of a system and an 84 | /// account as input. It retrieves a security identifier (SID) for the 85 | /// account and the name of the domain on which the account was found. 86 | /// 87 | /// 88 | /// The name of the system. 89 | /// 90 | /// 91 | /// The name of the account on the system. 92 | /// 93 | /// 94 | /// The pointer to a buffer that receives the SID structure. 95 | /// 96 | /// 97 | /// The size of the SID buffer. 98 | /// 99 | /// 100 | /// A buffer that received the name of the domain associated with 101 | /// the account. 102 | /// 103 | /// 104 | /// The size of the domain name buffer. 105 | /// 106 | /// 107 | /// A pointer to a SID_NAME_USER 108 | /// enum. 109 | /// 110 | /// 111 | /// If the function succeeds, the function returns nonzero. 112 | /// If the function fails, it returns zero. 113 | /// 114 | [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] 115 | public static extern bool LookupAccountName( 116 | string lpSystemName, 117 | string lpAccountName, 118 | [MarshalAs(UnmanagedType.LPArray)] byte[] Sid, 119 | ref uint cbSid, 120 | StringBuilder ReferencedDomainName, 121 | ref uint cchReferencedDomainName, 122 | out SID_NAME_USE peUse); 123 | 124 | /// 125 | /// converts a security identifier (SID) to a string format suitable 126 | /// for display, storage, or transmission. 127 | /// 128 | /// 129 | /// A pointer to the SID structure to be converted. 130 | /// 131 | /// 132 | /// A pointer to a variable that receives a pointer to a null-terminated 133 | /// SID string. 134 | /// 135 | /// 136 | /// If the function succeeds, the return value is nonzero. 137 | /// If the function fails, the return value is zero. 138 | /// 139 | [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] 140 | public static extern bool ConvertSidToStringSid( 141 | [MarshalAs(UnmanagedType.LPArray)] byte[] pSID, 142 | out IntPtr ptrSid); 143 | 144 | /// 145 | /// Frees the specified local memory object and invalidates its handle. 146 | /// 147 | /// 148 | /// A handle to the local memory object. 149 | /// 150 | /// 151 | /// If the function succeeds, the return value is NULL. 152 | /// If the function fails, the return value is equal to a handle to the 153 | /// local memory object. 154 | /// 155 | [DllImport("kernel32.dll")] 156 | public static extern IntPtr LocalFree(IntPtr hMem); 157 | #endregion 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /LocalSystem/WindowsUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Principal; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using Microsoft.Win32; 8 | 9 | namespace TE.LocalSystem 10 | { 11 | /// 12 | /// Description of WindowsUser. 13 | /// 14 | public class WindowsUser 15 | { 16 | #region Constants 17 | /// 18 | /// The root key for the users registry hive. 19 | /// 20 | private const string RegistryUsersRoot = "HKEY_USERS"; 21 | /// 22 | /// The registry key that stores the local application data folder for 23 | /// the Windows user. 24 | /// 25 | private const string RegistryLocalAppDataKey = 26 | @"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\"; 27 | /// 28 | /// The name of the local Plex data path registry value. 29 | /// 30 | private const string RegistryLocalAppDataValueName = "Local AppData"; 31 | #endregion 32 | 33 | #region Private Variables 34 | /// 35 | /// The Windows identity of the user. 36 | /// 37 | private WindowsIdentity userIdentity = null; 38 | #endregion 39 | 40 | #region Properties 41 | /// 42 | /// Gets the local application data folder for the user. 43 | /// 44 | public string LocalAppDataFolder { get; private set; } 45 | 46 | /// 47 | /// Gets or sets the name of the Windows user. 48 | /// 49 | public string Name { get; set; } 50 | 51 | /// 52 | /// Gets the SID associated with the user. 53 | /// 54 | public string Sid { get; set; } 55 | #endregion 56 | 57 | #region Constructors 58 | /// 59 | /// Creates an instance of the 60 | /// class. 61 | /// 62 | /// 63 | /// Thrown when the SID for the user is null or empty. 64 | /// 65 | public WindowsUser() 66 | { 67 | try 68 | { 69 | Initialize(string.Empty); 70 | } 71 | catch 72 | { 73 | throw; 74 | } 75 | } 76 | 77 | /// 78 | /// Creates an instance of the 79 | /// class when provided with the Windows user's name. 80 | /// 81 | /// 82 | /// Thrown when the SID for the user is null or empty. 83 | /// 84 | public WindowsUser(string name) 85 | { 86 | try 87 | { 88 | Initialize(name); 89 | } 90 | catch 91 | { 92 | throw; 93 | } 94 | } 95 | #endregion 96 | 97 | #region Private Functions 98 | /// 99 | /// Gets the Windows identity for the user. 100 | /// 101 | /// 102 | /// A object 103 | /// of the Windows user, or null if the 104 | /// object could not be created. 105 | /// 106 | private WindowsIdentity GetIdentity() 107 | { 108 | WindowsIdentity identity = null; 109 | 110 | try 111 | { 112 | if (string.IsNullOrWhiteSpace(Name)) 113 | { 114 | // If no Windows user name is specified, just get the identity 115 | // for the current user 116 | identity = WindowsIdentity.GetCurrent(); 117 | } 118 | else 119 | { 120 | // Get the identity for the user associated with the user name 121 | identity = new WindowsIdentity(Name); 122 | } 123 | } 124 | catch (UnauthorizedAccessException) 125 | { 126 | return null; 127 | } 128 | catch (System.Security.SecurityException) 129 | { 130 | return null; 131 | } 132 | 133 | return identity; 134 | } 135 | 136 | /// 137 | /// Gets the local application data path for the Windows user. 138 | /// 139 | /// 140 | /// Thrown when the SID for the user is null or empty. 141 | /// 142 | /// 143 | /// The path of the local application data folder for the Windows user. 144 | /// 145 | private string GetLocalAppDataFolder() 146 | { 147 | // Check to ensure a SID value for the user is set 148 | if (string.IsNullOrWhiteSpace(Sid)) 149 | { 150 | throw new WindowsUserSidNotFound( 151 | "The SID for the user was not specified."); 152 | } 153 | 154 | try 155 | { 156 | using (RegistryKey localAppDataRegistry = 157 | Registry.Users.OpenSubKey($"{Sid}\\{RegistryLocalAppDataKey}")) 158 | { 159 | return (string)localAppDataRegistry.GetValue(RegistryLocalAppDataValueName); 160 | } 161 | } 162 | catch 163 | { 164 | return null; 165 | } 166 | 167 | } 168 | 169 | /// 170 | /// Gets the SID for the associated Windows user. 171 | /// 172 | /// 173 | /// The SID for the Windows user. 174 | /// 175 | private string GetSid() 176 | { 177 | // Verify that the username was provided 178 | if (string.IsNullOrWhiteSpace(Name)) 179 | { 180 | return string.Empty; 181 | } 182 | 183 | try 184 | { 185 | // Get the account for the username 186 | NTAccount account = new NTAccount(Name); 187 | 188 | // Try to get the security identifier for the username 189 | SecurityIdentifier identifier = (SecurityIdentifier)account.Translate( 190 | typeof(SecurityIdentifier)); 191 | 192 | // Return the string value of the identifier 193 | return identifier.Value; 194 | } 195 | catch (IdentityNotMappedException) 196 | { 197 | // If the identity could not be mapped, return null 198 | return null; 199 | } 200 | } 201 | 202 | /// 203 | /// Gets the SID for the associated Windows user using the Windows API. 204 | /// 205 | /// 206 | /// The SID for the Windows user. 207 | /// 208 | private string GetSidApi() 209 | { 210 | // Verify that the username was provided 211 | if (string.IsNullOrWhiteSpace(Name)) 212 | { 213 | return string.Empty; 214 | } 215 | 216 | // The byte array that will hold the SID 217 | byte[] sid = null; 218 | // The SID buffer size 219 | uint cbSid = 0; 220 | // Then domain name 221 | StringBuilder referencedDomainName = new StringBuilder(); 222 | // The buffer size for the domain name 223 | uint cchReferencedDomainName = (uint)referencedDomainName.Capacity; 224 | // The type of SID 225 | WinApi.SID_NAME_USE sidUse; 226 | 227 | // Default the return value to indicate no error 228 | int err = WinApi.NO_ERROR; 229 | 230 | // Attempt to get the SID for the account name 231 | if (!WinApi.LookupAccountName( 232 | null, 233 | Name, 234 | sid, 235 | ref cbSid, 236 | referencedDomainName, 237 | ref cchReferencedDomainName, 238 | out sidUse)) 239 | { 240 | // Get the error from the LookupAccountName API call 241 | err = Marshal.GetLastWin32Error(); 242 | 243 | // Check to see if either the buffer wasn't sufficient or the 244 | // invalid flags error was returned 245 | if (err == WinApi.ERROR_INSUFFICIENT_BUFFER || err == WinApi.ERROR_INVALID_FLAGS) 246 | { 247 | // Create a new byte buffer with the size returned from the 248 | // first LookupAccountName request 249 | sid = new byte[cbSid]; 250 | 251 | // Make sure that the capacity of the StringBuilder object 252 | // for the domain name is at the correct buffer size 253 | referencedDomainName.EnsureCapacity((int)cchReferencedDomainName); 254 | 255 | // Reset the return value to indicate no error 256 | err = WinApi.NO_ERROR; 257 | 258 | // Attempt to get the account name after the correct buffer 259 | // sizes have been set 260 | if (!WinApi.LookupAccountName( 261 | null, 262 | Name, 263 | sid, 264 | ref cbSid, 265 | referencedDomainName, 266 | ref cchReferencedDomainName, 267 | out sidUse)) 268 | { 269 | // Return null if the SID could not be retrieved 270 | return null; 271 | } 272 | } 273 | } 274 | // Any other error that occured when trying to get the SID for the 275 | // account name 276 | else 277 | { 278 | // Return null if the SID could not be retrieved 279 | return null; 280 | } 281 | 282 | // No error occurred 283 | if (err == 0) 284 | { 285 | // Create the SID pointer 286 | IntPtr ptrSid; 287 | 288 | // Attempt to convert the SID byte array to a string 289 | if (!WinApi.ConvertSidToStringSid(sid, out ptrSid)) 290 | { 291 | // Return an empty string if the SID could not be 292 | // retrieved 293 | return null; 294 | } 295 | else 296 | { 297 | // Copy all characters from an unmanaged memory string to 298 | // a manage string 299 | string sidString = Marshal.PtrToStringAuto(ptrSid); 300 | // Free up the memory 301 | WinApi.LocalFree(ptrSid); 302 | 303 | // Return the SID for the account name 304 | return sidString; 305 | } 306 | } 307 | else 308 | { 309 | // Return null if the SID could not be retrieved 310 | return null; 311 | } 312 | } 313 | 314 | /// 315 | /// Gets the SID from the registry for the associated Windows user. 316 | /// 317 | /// 318 | /// The SID For the Windows user. 319 | /// 320 | private string GetSidRegistry() 321 | { 322 | // Verify that the username was provided 323 | if (string.IsNullOrWhiteSpace(Name)) 324 | { 325 | return null; 326 | } 327 | 328 | // Default to a blank SID 329 | string sid = null; 330 | 331 | // Remove the domain name from the Windows user name 332 | string name = RemoveDomainFromName(Name); 333 | 334 | // Open the registry key that contains the profiles 335 | RegistryKey key = Registry.LocalMachine.OpenSubKey( 336 | @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"); 337 | 338 | // Loop through each of the subkeys that contain the profiles 339 | foreach (string keyName in key.GetSubKeyNames()) 340 | { 341 | // Open the key 342 | RegistryKey sidKey = key.OpenSubKey(keyName); 343 | 344 | // Check to ensure the key was opened 345 | if (sidKey != null) 346 | { 347 | // Get the profile path value from the registry 348 | string profilePath = sidKey.GetValue("ProfileImagePath").ToString(); 349 | 350 | // Check to see if the account name is in the profile path 351 | if (profilePath.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) 352 | { 353 | // If the account name was in the profile path, store 354 | // the SID 355 | sid = System.IO.Path.GetFileName(sidKey.Name); 356 | 357 | } 358 | } 359 | } 360 | 361 | // Return the SID 362 | return sid; 363 | } 364 | 365 | /// 366 | /// Gets the SID of a well-known account. Such accounts are built into 367 | /// Windows and the SID for these accounts are the same on all 368 | /// computers. 369 | /// 370 | /// 371 | /// The SID for the Windows user. 372 | /// 373 | private string GetSidKnown() 374 | { 375 | // Verify that the username was provided 376 | if (string.IsNullOrWhiteSpace(Name)) 377 | { 378 | return null; 379 | } 380 | 381 | // Remove the domain name from the Windows user name 382 | string name = RemoveDomainFromName(Name); 383 | 384 | SecurityIdentifier identifier = null; 385 | 386 | try 387 | { 388 | 389 | if (name == "LocalService") 390 | { 391 | // Get the security identifier for the LocalService 392 | identifier = new SecurityIdentifier( 393 | WellKnownSidType.LocalServiceSid, 394 | null); 395 | } 396 | if (name == "LocalSystem") 397 | { 398 | // Get the security identifier for the LocalSystem 399 | identifier = new SecurityIdentifier( 400 | WellKnownSidType.LocalSystemSid, 401 | null); 402 | } 403 | else if (name == "NetworkService") 404 | { 405 | // Get the security identifier for the NetworkServer 406 | identifier = new SecurityIdentifier( 407 | WellKnownSidType.NetworkServiceSid, 408 | null); 409 | } 410 | 411 | return identifier.Value; 412 | 413 | } 414 | catch 415 | { 416 | return null; 417 | } 418 | } 419 | 420 | /// 421 | /// Initializes the objects and properties in the class. 422 | /// 423 | /// 424 | /// Thrown when the SID for the user is null or empty. 425 | /// 426 | private void Initialize(string name) 427 | { 428 | Name = name; 429 | userIdentity = GetIdentity(); 430 | 431 | if (string.IsNullOrWhiteSpace(Name)) 432 | { 433 | Name = (userIdentity == null) ? WindowsIdentity.GetCurrent().Name : Name = userIdentity.Name; 434 | } 435 | 436 | // Try to see if the account is a well-known account and get the 437 | // SID for the account 438 | Sid = GetSidKnown(); 439 | 440 | // If the the account name doesn't match a well-known account, 441 | // then try to find the SID for the account 442 | if (string.IsNullOrWhiteSpace(Sid)) 443 | { 444 | Sid = GetSid(); 445 | 446 | // If no SID was returned, try to get the SID using the 447 | // Windows API 448 | if (string.IsNullOrWhiteSpace(Sid)) 449 | { 450 | Sid = GetSidApi(); 451 | 452 | // If still no SID was returned, try to find it in the 453 | // registry 454 | if (string.IsNullOrWhiteSpace(Sid)) 455 | { 456 | Sid = GetSidRegistry(); 457 | } 458 | } 459 | } 460 | 461 | // Throw an exception is the SID was not found 462 | if (string.IsNullOrEmpty(Sid)) 463 | { 464 | throw new WindowsUserSidNotFound( 465 | "The SID for the user was not specified."); 466 | } 467 | 468 | LocalAppDataFolder = GetLocalAppDataFolder(); 469 | } 470 | 471 | /// 472 | /// Removes the domain name from the Windows user. 473 | /// 474 | /// 475 | /// The name of the Windows user 476 | /// 477 | /// 478 | /// The name of the Windows user without the domain name. 479 | /// 480 | private string RemoveDomainFromName(string name) 481 | { 482 | // Check to see if the name contains a slash 483 | if (name.Contains(@"\")) 484 | { 485 | // Remove the domain name 486 | name = Regex.Replace( 487 | name, 488 | @".*\\(.*)", 489 | "$1", 490 | RegexOptions.None); 491 | } 492 | 493 | // Return the name without the domain name 494 | return name; 495 | } 496 | #endregion 497 | 498 | #region Public Functions 499 | /// 500 | /// Checks to see if the user context running the application is an 501 | /// administrator. 502 | /// 503 | /// 504 | /// True if the user is an administrator, false if they are not an 505 | /// administrator. 506 | /// 507 | public bool IsAdministrator() 508 | { 509 | WindowsPrincipal principal = new WindowsPrincipal(userIdentity); 510 | return principal.IsInRole(WindowsBuiltInRole.Administrator); 511 | } 512 | #endregion 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /LocalSystem/WindowsUserSidNotFound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace TE.LocalSystem 5 | { 6 | /// 7 | /// The SID for the Windows user could not be found. 8 | /// 9 | public class WindowsUserSidNotFound : Exception 10 | { 11 | public WindowsUserSidNotFound() { } 12 | 13 | public WindowsUserSidNotFound(string message) 14 | : base(message) { } 15 | 16 | public WindowsUserSidNotFound( 17 | string message, 18 | Exception innerException) 19 | : base(message, innerException) { } 20 | 21 | protected WindowsUserSidNotFound( 22 | SerializationInfo info, 23 | StreamingContext context) 24 | : base(info, context) { } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using static System.Environment; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace TE 10 | { 11 | /// 12 | /// Contains the properties and methods to write a log file. 13 | /// 14 | public static class Log 15 | { 16 | /// 17 | /// The name of the updater log file. 18 | /// 19 | private const string LogFileName = "plex-updater.txt"; 20 | 21 | private static string _defaultFolder; 22 | 23 | /// 24 | /// Gets the the full path to the log file. 25 | /// 26 | public static string FilePath { get; private set; } 27 | 28 | /// 29 | /// Gets the folder to the log file. 30 | /// 31 | public static string Folder { get; private set; } 32 | 33 | /// 34 | /// Initializes an instance of the class. 35 | /// 36 | static Log() 37 | { 38 | _defaultFolder = Path.GetTempPath(); 39 | Folder = _defaultFolder; 40 | 41 | // Set the full path to the log file 42 | FilePath = Path.Combine(Folder, LogFileName); 43 | } 44 | 45 | /// 46 | /// Gets the formatted timestamp value. 47 | /// 48 | /// 49 | /// A string representation of the timestamp. 50 | /// 51 | private static string GetTimeStamp() 52 | { 53 | return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss "); 54 | } 55 | 56 | /// 57 | /// Deletes the log file. 58 | /// 59 | public static void Delete() 60 | { 61 | File.Delete(FilePath); 62 | } 63 | 64 | /// 65 | /// Sets the full path to the log file. 66 | /// 67 | /// 68 | /// The full path to the log file. 69 | /// 70 | public static void SetFolder(string path) 71 | { 72 | try 73 | { 74 | // Call this to validate the path 75 | Path.GetFullPath(path); 76 | 77 | Folder = Path.GetDirectoryName(path); 78 | if (!Directory.Exists(Folder)) 79 | { 80 | Directory.CreateDirectory(Folder); 81 | } 82 | 83 | FilePath = Path.Combine(Folder, LogFileName); 84 | } 85 | catch 86 | { 87 | Folder = _defaultFolder; 88 | FilePath = Path.Combine(Folder, LogFileName); 89 | } 90 | } 91 | 92 | /// 93 | /// Writes a string value to the log file. 94 | /// 95 | public static void Write(string text, bool appendDate = true) 96 | { 97 | string timeStamp = string.Empty; 98 | if (appendDate) 99 | { 100 | timeStamp = GetTimeStamp(); 101 | } 102 | 103 | File.AppendAllText(FilePath, $"{timeStamp}{text}{NewLine}"); 104 | } 105 | 106 | /// 107 | /// Writes information about an exception to the log file. 108 | /// 109 | /// 110 | /// The object that contains information to write to 111 | /// the log file. 112 | /// 113 | public static void Write(Exception ex, bool appendDate = true) 114 | { 115 | if (ex == null) 116 | { 117 | return; 118 | } 119 | 120 | string timeStamp = string.Empty; 121 | if (appendDate) 122 | { 123 | timeStamp = GetTimeStamp(); 124 | } 125 | 126 | File.AppendAllText( 127 | FilePath, 128 | $"{timeStamp}Message:{NewLine}{ex.Message}{NewLine}{NewLine}Inner Exception:{NewLine}{ex.InnerException}{NewLine}{NewLine}Stack Trace:{NewLine}{ex.StackTrace}{NewLine}"); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by SharpDevelop. 3 | * User: Paul 4 | * Date: 2/22/2016 5 | * Time: 7:50 PM 6 | * 7 | * To change this template use Tools | Options | Coding | Edit Standard Headers. 8 | */ 9 | namespace TE.Plex 10 | { 11 | partial class MainForm 12 | { 13 | /// 14 | /// Designer variable used to keep track of non-visual components. 15 | /// 16 | private System.ComponentModel.IContainer components = null; 17 | private System.Windows.Forms.Button btnCancel; 18 | private System.Windows.Forms.Button btnUpdate; 19 | private System.Windows.Forms.GroupBox grpUpdateStatus; 20 | private System.Windows.Forms.TextBox txtUpdateStatus; 21 | private System.Windows.Forms.GroupBox groupBox1; 22 | private System.Windows.Forms.Label lblLatestVersion; 23 | private System.Windows.Forms.Label lblInstalledVersion; 24 | private System.Windows.Forms.Label lblLatestVersionLabel; 25 | private System.Windows.Forms.Label lblInstalledVersionLabel; 26 | 27 | /// 28 | /// Disposes resources used by the form. 29 | /// 30 | /// true if managed resources should be disposed; otherwise, false. 31 | protected override void Dispose(bool disposing) 32 | { 33 | if (disposing) { 34 | if (components != null) { 35 | components.Dispose(); 36 | } 37 | } 38 | base.Dispose(disposing); 39 | } 40 | 41 | /// 42 | /// This method is required for Windows Forms designer support. 43 | /// Do not change the method contents inside the source code editor. The Forms designer might 44 | /// not be able to load this method if it was changed manually. 45 | /// 46 | private void InitializeComponent() 47 | { 48 | this.btnCancel = new System.Windows.Forms.Button(); 49 | this.btnUpdate = new System.Windows.Forms.Button(); 50 | this.grpUpdateStatus = new System.Windows.Forms.GroupBox(); 51 | this.txtUpdateStatus = new System.Windows.Forms.TextBox(); 52 | this.groupBox1 = new System.Windows.Forms.GroupBox(); 53 | this.lblPlayCount = new System.Windows.Forms.Label(); 54 | this.lblPlayCountLabel = new System.Windows.Forms.Label(); 55 | this.lblLatestVersion = new System.Windows.Forms.Label(); 56 | this.lblInstalledVersion = new System.Windows.Forms.Label(); 57 | this.lblLatestVersionLabel = new System.Windows.Forms.Label(); 58 | this.lblInstalledVersionLabel = new System.Windows.Forms.Label(); 59 | this.btnExit = new System.Windows.Forms.Button(); 60 | this.chkWait = new System.Windows.Forms.CheckBox(); 61 | this.lblCheckEveryLabel = new System.Windows.Forms.Label(); 62 | this.numSeconds = new System.Windows.Forms.NumericUpDown(); 63 | this.lblCheckSecondsLabel = new System.Windows.Forms.Label(); 64 | this.lblInProgressRecordingCount = new System.Windows.Forms.Label(); 65 | this.lblInProgressRecordingCountLabel = new System.Windows.Forms.Label(); 66 | this.grpUpdateStatus.SuspendLayout(); 67 | this.groupBox1.SuspendLayout(); 68 | ((System.ComponentModel.ISupportInitialize)(this.numSeconds)).BeginInit(); 69 | this.SuspendLayout(); 70 | // 71 | // btnCancel 72 | // 73 | this.btnCancel.Location = new System.Drawing.Point(476, 448); 74 | this.btnCancel.Name = "btnCancel"; 75 | this.btnCancel.Size = new System.Drawing.Size(75, 23); 76 | this.btnCancel.TabIndex = 1; 77 | this.btnCancel.Text = "Cancel"; 78 | this.btnCancel.UseVisualStyleBackColor = true; 79 | this.btnCancel.Visible = false; 80 | this.btnCancel.Click += new System.EventHandler(this.BtnCancelClick); 81 | // 82 | // btnUpdate 83 | // 84 | this.btnUpdate.Location = new System.Drawing.Point(394, 448); 85 | this.btnUpdate.Name = "btnUpdate"; 86 | this.btnUpdate.Size = new System.Drawing.Size(75, 23); 87 | this.btnUpdate.TabIndex = 2; 88 | this.btnUpdate.Text = "Update"; 89 | this.btnUpdate.UseVisualStyleBackColor = true; 90 | this.btnUpdate.Click += new System.EventHandler(this.BtnUpdateClick); 91 | // 92 | // grpUpdateStatus 93 | // 94 | this.grpUpdateStatus.Controls.Add(this.txtUpdateStatus); 95 | this.grpUpdateStatus.Location = new System.Drawing.Point(12, 136); 96 | this.grpUpdateStatus.Name = "grpUpdateStatus"; 97 | this.grpUpdateStatus.Size = new System.Drawing.Size(538, 289); 98 | this.grpUpdateStatus.TabIndex = 3; 99 | this.grpUpdateStatus.TabStop = false; 100 | this.grpUpdateStatus.Text = "Update Status:"; 101 | // 102 | // txtUpdateStatus 103 | // 104 | this.txtUpdateStatus.Location = new System.Drawing.Point(7, 20); 105 | this.txtUpdateStatus.Multiline = true; 106 | this.txtUpdateStatus.Name = "txtUpdateStatus"; 107 | this.txtUpdateStatus.ScrollBars = System.Windows.Forms.ScrollBars.Both; 108 | this.txtUpdateStatus.Size = new System.Drawing.Size(525, 263); 109 | this.txtUpdateStatus.TabIndex = 0; 110 | // 111 | // groupBox1 112 | // 113 | this.groupBox1.Controls.Add(this.lblInProgressRecordingCount); 114 | this.groupBox1.Controls.Add(this.lblInProgressRecordingCountLabel); 115 | this.groupBox1.Controls.Add(this.lblPlayCount); 116 | this.groupBox1.Controls.Add(this.lblPlayCountLabel); 117 | this.groupBox1.Controls.Add(this.lblLatestVersion); 118 | this.groupBox1.Controls.Add(this.lblInstalledVersion); 119 | this.groupBox1.Controls.Add(this.lblLatestVersionLabel); 120 | this.groupBox1.Controls.Add(this.lblInstalledVersionLabel); 121 | this.groupBox1.Location = new System.Drawing.Point(13, 13); 122 | this.groupBox1.Name = "groupBox1"; 123 | this.groupBox1.Size = new System.Drawing.Size(537, 116); 124 | this.groupBox1.TabIndex = 4; 125 | this.groupBox1.TabStop = false; 126 | this.groupBox1.Text = "Plex Media Server Information"; 127 | // 128 | // lblPlayCount 129 | // 130 | this.lblPlayCount.Location = new System.Drawing.Point(178, 66); 131 | this.lblPlayCount.Name = "lblPlayCount"; 132 | this.lblPlayCount.Size = new System.Drawing.Size(100, 17); 133 | this.lblPlayCount.TabIndex = 7; 134 | this.lblPlayCount.Text = "[]"; 135 | // 136 | // lblPlayCountLabel 137 | // 138 | this.lblPlayCountLabel.AutoSize = true; 139 | this.lblPlayCountLabel.Location = new System.Drawing.Point(7, 66); 140 | this.lblPlayCountLabel.Name = "lblPlayCountLabel"; 141 | this.lblPlayCountLabel.Size = new System.Drawing.Size(165, 13); 142 | this.lblPlayCountLabel.TabIndex = 6; 143 | this.lblPlayCountLabel.Text = "Number of items currently playing:"; 144 | // 145 | // lblLatestVersion 146 | // 147 | this.lblLatestVersion.Location = new System.Drawing.Point(178, 43); 148 | this.lblLatestVersion.Name = "lblLatestVersion"; 149 | this.lblLatestVersion.Size = new System.Drawing.Size(100, 13); 150 | this.lblLatestVersion.TabIndex = 3; 151 | this.lblLatestVersion.Text = "[]"; 152 | // 153 | // lblInstalledVersion 154 | // 155 | this.lblInstalledVersion.Location = new System.Drawing.Point(178, 20); 156 | this.lblInstalledVersion.Name = "lblInstalledVersion"; 157 | this.lblInstalledVersion.Size = new System.Drawing.Size(100, 13); 158 | this.lblInstalledVersion.TabIndex = 2; 159 | this.lblInstalledVersion.Text = "[]"; 160 | // 161 | // lblLatestVersionLabel 162 | // 163 | this.lblLatestVersionLabel.Location = new System.Drawing.Point(7, 43); 164 | this.lblLatestVersionLabel.Name = "lblLatestVersionLabel"; 165 | this.lblLatestVersionLabel.Size = new System.Drawing.Size(100, 13); 166 | this.lblLatestVersionLabel.TabIndex = 1; 167 | this.lblLatestVersionLabel.Text = "Latest Version:"; 168 | // 169 | // lblInstalledVersionLabel 170 | // 171 | this.lblInstalledVersionLabel.Location = new System.Drawing.Point(7, 20); 172 | this.lblInstalledVersionLabel.Name = "lblInstalledVersionLabel"; 173 | this.lblInstalledVersionLabel.Size = new System.Drawing.Size(99, 13); 174 | this.lblInstalledVersionLabel.TabIndex = 0; 175 | this.lblInstalledVersionLabel.Text = "Installed Version:"; 176 | // 177 | // btnExit 178 | // 179 | this.btnExit.Location = new System.Drawing.Point(475, 448); 180 | this.btnExit.Name = "btnExit"; 181 | this.btnExit.Size = new System.Drawing.Size(75, 23); 182 | this.btnExit.TabIndex = 5; 183 | this.btnExit.Text = "Exit"; 184 | this.btnExit.UseVisualStyleBackColor = true; 185 | this.btnExit.Click += new System.EventHandler(this.btnExit_Click); 186 | // 187 | // chkWait 188 | // 189 | this.chkWait.AutoSize = true; 190 | this.chkWait.Checked = true; 191 | this.chkWait.CheckState = System.Windows.Forms.CheckState.Checked; 192 | this.chkWait.Location = new System.Drawing.Point(19, 431); 193 | this.chkWait.Name = "chkWait"; 194 | this.chkWait.Size = new System.Drawing.Size(161, 17); 195 | this.chkWait.TabIndex = 6; 196 | this.chkWait.Text = "Only update when not in use"; 197 | this.chkWait.UseVisualStyleBackColor = true; 198 | this.chkWait.CheckedChanged += new System.EventHandler(this.chkWait_CheckedChanged); 199 | // 200 | // lblCheckEveryLabel 201 | // 202 | this.lblCheckEveryLabel.AutoSize = true; 203 | this.lblCheckEveryLabel.Location = new System.Drawing.Point(16, 458); 204 | this.lblCheckEveryLabel.Name = "lblCheckEveryLabel"; 205 | this.lblCheckEveryLabel.Size = new System.Drawing.Size(67, 13); 206 | this.lblCheckEveryLabel.TabIndex = 7; 207 | this.lblCheckEveryLabel.Text = "Check every"; 208 | // 209 | // numSeconds 210 | // 211 | this.numSeconds.Location = new System.Drawing.Point(89, 456); 212 | this.numSeconds.Maximum = new decimal(new int[] { 213 | 3600, 214 | 0, 215 | 0, 216 | 0}); 217 | this.numSeconds.Minimum = new decimal(new int[] { 218 | 5, 219 | 0, 220 | 0, 221 | 0}); 222 | this.numSeconds.Name = "numSeconds"; 223 | this.numSeconds.Size = new System.Drawing.Size(53, 20); 224 | this.numSeconds.TabIndex = 8; 225 | this.numSeconds.Value = new decimal(new int[] { 226 | 30, 227 | 0, 228 | 0, 229 | 0}); 230 | // 231 | // lblCheckSecondsLabel 232 | // 233 | this.lblCheckSecondsLabel.AutoSize = true; 234 | this.lblCheckSecondsLabel.Location = new System.Drawing.Point(148, 458); 235 | this.lblCheckSecondsLabel.Name = "lblCheckSecondsLabel"; 236 | this.lblCheckSecondsLabel.Size = new System.Drawing.Size(47, 13); 237 | this.lblCheckSecondsLabel.TabIndex = 9; 238 | this.lblCheckSecondsLabel.Text = "seconds"; 239 | // 240 | // lblInProgressRecordingCount 241 | // 242 | this.lblInProgressRecordingCount.Location = new System.Drawing.Point(178, 89); 243 | this.lblInProgressRecordingCount.Name = "lblInProgressRecordingCount"; 244 | this.lblInProgressRecordingCount.Size = new System.Drawing.Size(100, 17); 245 | this.lblInProgressRecordingCount.TabIndex = 9; 246 | this.lblInProgressRecordingCount.Text = "[]"; 247 | // 248 | // lblInProgressRecordingCountLabel 249 | // 250 | this.lblInProgressRecordingCountLabel.AutoSize = true; 251 | this.lblInProgressRecordingCountLabel.Location = new System.Drawing.Point(7, 89); 252 | this.lblInProgressRecordingCountLabel.Name = "lblInProgressRecordingCountLabel"; 253 | this.lblInProgressRecordingCountLabel.Size = new System.Drawing.Size(165, 13); 254 | this.lblInProgressRecordingCountLabel.TabIndex = 8; 255 | this.lblInProgressRecordingCountLabel.Text = "Number of in progress recordings:"; 256 | // 257 | // MainForm 258 | // 259 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 260 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 261 | this.ClientSize = new System.Drawing.Size(563, 490); 262 | this.Controls.Add(this.lblCheckSecondsLabel); 263 | this.Controls.Add(this.numSeconds); 264 | this.Controls.Add(this.lblCheckEveryLabel); 265 | this.Controls.Add(this.chkWait); 266 | this.Controls.Add(this.btnExit); 267 | this.Controls.Add(this.groupBox1); 268 | this.Controls.Add(this.grpUpdateStatus); 269 | this.Controls.Add(this.btnUpdate); 270 | this.Controls.Add(this.btnCancel); 271 | this.MaximizeBox = false; 272 | this.MinimizeBox = false; 273 | this.Name = "MainForm"; 274 | this.ShowIcon = false; 275 | this.ShowInTaskbar = false; 276 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 277 | this.Text = "Plex Server Updater"; 278 | this.Shown += new System.EventHandler(this.MainForm_Shown); 279 | this.grpUpdateStatus.ResumeLayout(false); 280 | this.grpUpdateStatus.PerformLayout(); 281 | this.groupBox1.ResumeLayout(false); 282 | this.groupBox1.PerformLayout(); 283 | ((System.ComponentModel.ISupportInitialize)(this.numSeconds)).EndInit(); 284 | this.ResumeLayout(false); 285 | this.PerformLayout(); 286 | 287 | } 288 | 289 | private System.Windows.Forms.Button btnExit; 290 | private System.Windows.Forms.Label lblPlayCount; 291 | private System.Windows.Forms.Label lblPlayCountLabel; 292 | private System.Windows.Forms.CheckBox chkWait; 293 | private System.Windows.Forms.Label lblCheckEveryLabel; 294 | private System.Windows.Forms.NumericUpDown numSeconds; 295 | private System.Windows.Forms.Label lblCheckSecondsLabel; 296 | private System.Windows.Forms.Label lblInProgressRecordingCount; 297 | private System.Windows.Forms.Label lblInProgressRecordingCountLabel; 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /MainForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using static System.Environment; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Timers; 8 | using System.Windows.Forms; 9 | 10 | namespace TE.Plex 11 | { 12 | /// 13 | /// The Plex Media Server Updater main form. 14 | /// 15 | public partial class MainForm : Form 16 | { 17 | #region Private Variables 18 | /// 19 | /// The media server object. 20 | /// 21 | private MediaServer _server = null; 22 | 23 | /// 24 | /// The cancellation token source. 25 | /// 26 | private CancellationTokenSource _cts = null; 27 | 28 | /// 29 | /// The wait timer. 30 | /// 31 | private System.Timers.Timer _timer = null; 32 | #endregion 33 | 34 | #region Properties 35 | /// 36 | /// Gets a flag indicating if the form is to be closed. 37 | /// 38 | public bool ToBeClosed { get; private set; } 39 | #endregion 40 | 41 | #region Constructors 42 | /// 43 | /// Initializes the form. 44 | /// 45 | public MainForm() 46 | { 47 | InitializeComponent(); 48 | Initialize(); 49 | } 50 | #endregion 51 | 52 | #region Events 53 | /// 54 | /// Cancels the Plex update.. 55 | /// 56 | /// 57 | /// The sender. 58 | /// 59 | /// 60 | /// Event-related arguments. 61 | /// 62 | void BtnCancelClick(object sender, EventArgs e) 63 | { 64 | _cts?.Cancel(); 65 | } 66 | 67 | /// 68 | /// Close the form. 69 | /// 70 | /// 71 | /// The sender. 72 | /// 73 | /// 74 | /// Event-related arguments. 75 | /// 76 | private void btnExit_Click(object sender, EventArgs e) 77 | { 78 | Log.Write("Closing the application."); 79 | Close(); 80 | } 81 | 82 | /// 83 | /// Performs the Plex Media Server update. 84 | /// 85 | /// 86 | /// The sender. 87 | /// 88 | /// 89 | /// Event-related arguments. 90 | /// 91 | void BtnUpdateClick(object sender, EventArgs e) 92 | { 93 | if (CheckIfCanUpdate()) 94 | { 95 | PerformUpdate(); 96 | } 97 | } 98 | 99 | /// 100 | /// Enables or disables the controls and timers on the form. 101 | /// 102 | /// 103 | /// The sender. 104 | /// 105 | /// 106 | /// Event-related arguments. 107 | /// 108 | private void chkWait_CheckedChanged(object sender, EventArgs e) 109 | { 110 | lblCheckEveryLabel.Enabled = chkWait.Checked; 111 | lblCheckSecondsLabel.Enabled = chkWait.Checked; 112 | numSeconds.Enabled = chkWait.Checked; 113 | if (_timer != null) 114 | { 115 | _timer.Enabled = chkWait.Checked; 116 | } 117 | } 118 | 119 | /// 120 | /// The form is shown. 121 | /// 122 | /// 123 | /// The sender. 124 | /// 125 | /// 126 | /// Arguments associated with the event. 127 | /// 128 | private void MainForm_Shown(object sender, EventArgs e) 129 | { 130 | // If the form is to be closed, then close the form. 131 | if (ToBeClosed) 132 | { 133 | Log.Write("Closing the application."); 134 | Close(); 135 | } 136 | } 137 | 138 | /// 139 | /// The messages from the update execution. 140 | /// 141 | /// 142 | /// The sender object. 143 | /// 144 | /// 145 | /// The message to display on the form. 146 | /// 147 | private void ServerUpdateMessage(object sender, string message) 148 | { 149 | txtUpdateStatus.Text += $"{message}{NewLine}"; 150 | Log.Write(message); 151 | } 152 | 153 | /// 154 | /// The timer has elapsed so check the play count to see if the server 155 | /// is in use. 156 | /// 157 | /// 158 | /// The sender. 159 | /// 160 | /// 163 | private void OnTimedEvent(object sender, ElapsedEventArgs e) 164 | { 165 | if (_server == null) 166 | { 167 | _timer.Enabled = false; 168 | return; 169 | } 170 | 171 | if (CheckIfCanUpdate()) 172 | { 173 | PerformUpdate(); 174 | } 175 | } 176 | #endregion 177 | 178 | #region Private Functions 179 | /// 180 | /// Checks to see if the server can be updated at this time. 181 | /// 182 | private bool CheckIfCanUpdate() 183 | { 184 | if (_server == null) 185 | { 186 | txtUpdateStatus.Text += "The server was not specified. Cannot perform the update."; 187 | Log.Write("The server was not specified. Cannot perform the update."); 188 | _timer.Enabled = false; 189 | return false; 190 | } 191 | 192 | int playCount = _server.GetPlayCount(); 193 | int inProgressRecordingCount = _server.GetInProgressRecordingCount(); 194 | 195 | // No item is currently being played 196 | if (playCount == 0 && inProgressRecordingCount == 0) 197 | { 198 | txtUpdateStatus.Text += "The server is not in use continuing to perform the update."; 199 | Log.Write("The server is not in use continuing to perform the update."); 200 | lblPlayCount.Text = playCount.ToString(); 201 | lblInProgressRecordingCount.Text = inProgressRecordingCount.ToString(); 202 | btnUpdate.Enabled = true; 203 | _timer.Enabled = false; 204 | return true; 205 | } 206 | // At least one item is being played 207 | else if (playCount > 0 || inProgressRecordingCount > 0) 208 | { 209 | lblPlayCount.Text = playCount.ToString(); 210 | lblInProgressRecordingCount.Text = inProgressRecordingCount.ToString(); 211 | if (chkWait.Checked) 212 | { 213 | txtUpdateStatus.Text += "Waiting for the server to be free has been enabled. Server update can begin. Waiting for all media and/or in progress recordings to be stopped."; 214 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update."); 215 | btnUpdate.Enabled = false; 216 | _timer.Interval = 217 | Convert.ToDouble(Math.Abs(numSeconds.Value) * 1000); 218 | _timer.Enabled = true; 219 | return false; 220 | } 221 | else if (!chkWait.Checked && inProgressRecordingCount > 0) 222 | { 223 | txtUpdateStatus.Text += "The wait option has been disabled, but you cannot update the server while there is a recording in progress. Waiting for all in progress recordings to be stopped."; 224 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update."); 225 | btnUpdate.Enabled = false; 226 | _timer.Interval = 227 | Convert.ToDouble(Math.Abs(numSeconds.Value) * 1000); 228 | _timer.Enabled = true; 229 | return false; 230 | } 231 | else 232 | { 233 | txtUpdateStatus.Text += "The wait option has been disabled. You can go ahead and update the server."; 234 | Log.Write("The wait option has been disabled. You can go ahead and update the server."); 235 | btnUpdate.Enabled = true; 236 | _timer.Enabled = false; 237 | return true; 238 | } 239 | } 240 | // Could not determine how many items are being played 241 | else 242 | { 243 | txtUpdateStatus.Text += "The server in use status could not be determined. The server can be updated if you wish."; 244 | Log.Write("The server in use status could not be determined. The server can be updated if you wish."); 245 | lblPlayCount.Text = "Unknown"; 246 | lblInProgressRecordingCount.Text = "Unknown"; 247 | btnUpdate.Enabled = true; 248 | _timer.Enabled = false; 249 | return true; 250 | } 251 | } 252 | 253 | /// 254 | /// Initializes the values on the form. 255 | /// 256 | private void Initialize() 257 | { 258 | try 259 | { 260 | ToBeClosed = false; 261 | _cts = new CancellationTokenSource(); 262 | 263 | Log.Write("Initializing the timer object."); 264 | _timer = new System.Timers.Timer(); 265 | _timer.Elapsed += new ElapsedEventHandler(OnTimedEvent); 266 | _timer.Enabled = false; 267 | _timer.Interval = Convert.ToDouble(numSeconds.Value * 1000); 268 | 269 | Log.Write("Initializing the Plex media server object."); 270 | _server = new MediaServer(ServerUpdateMessage); 271 | 272 | if (_server == null) 273 | { 274 | Log.Write( 275 | "The Plex media server object could not be initialized. Setting the flag to close the application."); 276 | ToBeClosed = true; 277 | return; 278 | } 279 | 280 | lblInstalledVersion.Text = _server.CurrentVersion.ToString(); 281 | lblLatestVersion.Text = _server.LatestVersion.ToString(); 282 | 283 | if (_server.LatestVersion > _server.CurrentVersion) 284 | { 285 | btnUpdate.Visible = true; 286 | btnCancel.Visible = false; 287 | btnExit.Enabled = true; 288 | CheckIfCanUpdate(); 289 | } 290 | else 291 | { 292 | btnUpdate.Visible = false; 293 | btnCancel.Visible = false; 294 | btnExit.Enabled = true; 295 | if (_server.GetPlayCount() >= 0 || _server.GetInProgressRecordingCount() >= 0) 296 | { 297 | lblPlayCount.Text = _server.PlayCount.ToString(); 298 | lblInProgressRecordingCount.Text = _server.InProgressRecordingCount.ToString(); 299 | } 300 | else 301 | { 302 | lblPlayCount.Text = "Unknown"; 303 | lblInProgressRecordingCount.Text = "Unknown"; 304 | } 305 | } 306 | } 307 | catch (LocalSystem.Msi.MSIException ex) 308 | { 309 | MessageBox.Show( 310 | $"MSI exception: {ex.Message}", 311 | "Plex Updater Error", 312 | MessageBoxButtons.OK, 313 | MessageBoxIcon.Error); 314 | Log.Write(ex); 315 | ToBeClosed = true; 316 | 317 | } 318 | catch (AppNotInstalledException ex) 319 | { 320 | MessageBox.Show( 321 | "The Plex Server application is not installed.", 322 | "Plex Updater Error", 323 | MessageBoxButtons.OK, 324 | MessageBoxIcon.Error); 325 | Log.Write(ex); 326 | ToBeClosed = true; 327 | } 328 | catch (ServiceNotInstalledException ex) 329 | { 330 | MessageBox.Show( 331 | "The Plex service is not installed.", 332 | "Plex Updater Error", 333 | MessageBoxButtons.OK, 334 | MessageBoxIcon.Error); 335 | Log.Write(ex); 336 | ToBeClosed = true; 337 | } 338 | catch (PlexDataFolderNotFoundException ex) 339 | { 340 | MessageBox.Show( 341 | "The Plex data folder could not be found.", 342 | "Plex Updater Error", 343 | MessageBoxButtons.OK, 344 | MessageBoxIcon.Error); 345 | Log.Write(ex); 346 | ToBeClosed = true; 347 | } 348 | catch (LocalSystem.WindowsUserSidNotFound ex) 349 | { 350 | MessageBox.Show( 351 | "The SID for the Plex service user could not be found.", 352 | "Plex Updater Error", 353 | MessageBoxButtons.OK, 354 | MessageBoxIcon.Error); 355 | Log.Write(ex); 356 | ToBeClosed = true; 357 | } 358 | 359 | catch (Exception ex) 360 | { 361 | MessageBox.Show( 362 | ex.Message, 363 | "Plex Updater Error", 364 | MessageBoxButtons.OK, 365 | MessageBoxIcon.Error); 366 | Log.Write(ex); 367 | ToBeClosed = true; 368 | } 369 | } 370 | 371 | /// 372 | /// Perform the server update. 373 | /// 374 | private void PerformUpdate() 375 | { 376 | try 377 | { 378 | btnUpdate.Enabled = false; 379 | btnCancel.Visible = false; 380 | btnExit.Enabled = false; 381 | 382 | if (_cts == null) 383 | { 384 | _cts = new CancellationTokenSource(); 385 | } 386 | 387 | CancellationToken ct = _cts.Token; 388 | 389 | Task plexUpdate = Task.Factory.StartNew(() => 390 | { 391 | // Throw an exception if already cancelled 392 | ct.ThrowIfCancellationRequested(); 393 | 394 | _server.Update(); 395 | }, _cts.Token); 396 | 397 | plexUpdate.Wait(); 398 | 399 | if (!_server.IsRunning()) 400 | { 401 | Log.Write("The Plex server was not started successfully."); 402 | } 403 | } 404 | catch (Exception ex) 405 | { 406 | txtUpdateStatus.Text += $"ERROR: {ex.Message}{NewLine}"; 407 | Log.Write(ex); 408 | } 409 | finally 410 | { 411 | _cts?.Dispose(); 412 | Initialize(); 413 | } 414 | } 415 | #endregion 416 | 417 | 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /MainForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /Plex/Api.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | using System.Xml.Linq; 10 | using System.Xml.Serialization; 11 | 12 | namespace TE.Plex 13 | { 14 | public class Api 15 | { 16 | #region Event Delegates 17 | /// 18 | /// The delegate for the Message event handler. 19 | /// 20 | /// 21 | /// The object that triggered the event. 22 | /// 23 | /// 24 | /// The message. 25 | /// 26 | public delegate void MessageChangedEventHandler(object sender, string messagee); 27 | #endregion 28 | 29 | #region Events 30 | /// 31 | /// The MessageChanged event member. 32 | /// 33 | public event MessageChangedEventHandler MessageChanged; 34 | 35 | /// 36 | /// Triggered when the message has changed. 37 | /// 38 | protected virtual void OnMessageChanged(string message) 39 | { 40 | MessageChanged?.Invoke(this, message); 41 | } 42 | #endregion 43 | 44 | #region Public Constants 45 | /// 46 | /// A constant representing an unknown value. 47 | /// 48 | public const int Unknown = -1; 49 | #endregion 50 | 51 | #region Private Variables 52 | /// 53 | /// The Plex server. 54 | /// 55 | private string _server; 56 | 57 | /// 58 | /// The Plex user token. 59 | /// 60 | private string _token; 61 | 62 | /// 63 | /// The HTTP client used to connect to the Plex website. 64 | /// 65 | private HttpClient _client = new HttpClient(); 66 | #endregion 67 | 68 | #region Constructors 69 | /// 70 | /// Creates an instance of the class when provided 71 | /// with the server name or IP address, and the Plex token. 72 | /// 73 | /// 74 | /// The name or IP address of the Plex server. 75 | /// 76 | /// 77 | /// The user's Plex token. 78 | /// 79 | public Api(string server, string token) 80 | { 81 | _server = server; 82 | _token = token; 83 | } 84 | #endregion 85 | 86 | #region Public Functions 87 | /// 88 | /// Gets the number of media currently being played on the Plex server. 89 | /// 90 | /// 91 | /// The number of items being played. 92 | /// 93 | public int GetPlayCount() 94 | { 95 | int playCount = Unknown; 96 | if (string.IsNullOrWhiteSpace(_server)) 97 | { 98 | OnMessageChanged("The Plex server was not provided so the play count could not be retrieved."); 99 | return playCount; 100 | } 101 | 102 | if (string.IsNullOrWhiteSpace(_token)) 103 | { 104 | OnMessageChanged("The Plex token was not provided so the play count could not be retrieved."); 105 | return playCount; 106 | } 107 | 108 | string url = $"http://{_server}:32400/status/sessions?X-Plex-Token={_token}"; 109 | string content = null; 110 | try 111 | { 112 | using (HttpResponseMessage response = _client.GetAsync(url).Result) 113 | { 114 | if (response.StatusCode == System.Net.HttpStatusCode.OK) 115 | { 116 | content = response.Content.ReadAsStringAsync().Result; 117 | } 118 | else 119 | { 120 | OnMessageChanged($"The connection to the Plex server wasn't successful. Status: {response.StatusCode.ToString()}."); 121 | } 122 | } 123 | } 124 | catch (Exception ex) 125 | when (ex is ArgumentNullException || ex is HttpRequestException) 126 | { 127 | OnMessageChanged($"There was an issue sending the request to the Plex server. Reason: {ex.Message}"); 128 | return playCount; 129 | } 130 | catch (AggregateException ae) 131 | { 132 | foreach (var e in ae.Flatten().InnerExceptions) 133 | { 134 | OnMessageChanged($"Could not process the response result. Message: {e.Message}."); 135 | } 136 | return playCount; 137 | } 138 | 139 | if (string.IsNullOrWhiteSpace(content)) 140 | { 141 | OnMessageChanged("No content was returned from the Plex server."); 142 | return playCount; 143 | } 144 | 145 | using (StringReader sr = new StringReader(content)) 146 | { 147 | XmlSerializer serializer = 148 | new XmlSerializer(typeof(MediaContainer)); 149 | try 150 | { 151 | MediaContainer mediaContainer = 152 | (MediaContainer)serializer.Deserialize(sr); 153 | playCount = Convert.ToInt32(mediaContainer.Size); 154 | } 155 | catch (Exception ex) 156 | when (ex is InvalidOperationException || ex is FormatException || ex is OverflowException) 157 | { 158 | OnMessageChanged($"The content could not be parsed. Reason: {ex.Message}"); 159 | return playCount; 160 | } 161 | } 162 | 163 | return playCount; 164 | } 165 | 166 | /// 167 | /// Gets the number of in progress recordings (i.e. by the DVR) on the Plex server. 168 | /// 169 | /// /// 170 | /// The number of items being currently recorded. 171 | /// 172 | public int GetInProgressRecordingCount() 173 | { 174 | int inProgressRecordingCount = Unknown; 175 | if (string.IsNullOrWhiteSpace(_server)) 176 | { 177 | OnMessageChanged("The Plex server was not provided so the in progress recording count could not be retrieved."); 178 | return inProgressRecordingCount; 179 | } 180 | 181 | if (string.IsNullOrWhiteSpace(_token)) 182 | { 183 | OnMessageChanged("The Plex token was not provided so the in progress recording count could not be retrieved."); 184 | return inProgressRecordingCount; 185 | } 186 | 187 | string url = $"http://{_server}:32400/media/subscriptions/scheduled?X-Plex-Token={_token}"; 188 | string content = null; 189 | try 190 | { 191 | using (HttpResponseMessage response = _client.GetAsync(url).Result) 192 | { 193 | if (response.StatusCode == System.Net.HttpStatusCode.OK) 194 | { 195 | content = response.Content.ReadAsStringAsync().Result; 196 | } 197 | else 198 | { 199 | OnMessageChanged($"The connection to the Plex server wasn't successful. Status: {response.StatusCode.ToString()}."); 200 | } 201 | } 202 | } 203 | catch (Exception ex) 204 | when (ex is ArgumentNullException || ex is HttpRequestException) 205 | { 206 | OnMessageChanged($"There was an issue sending the request to the Plex server. Reason: {ex.Message}"); 207 | return inProgressRecordingCount; 208 | } 209 | catch (AggregateException ae) 210 | { 211 | foreach (var e in ae.Flatten().InnerExceptions) 212 | { 213 | OnMessageChanged($"Could not process the response result. Message: {e.Message}."); 214 | } 215 | return inProgressRecordingCount; 216 | } 217 | 218 | if (string.IsNullOrWhiteSpace(content)) 219 | { 220 | OnMessageChanged("No content was returned from the Plex server."); 221 | return inProgressRecordingCount; 222 | } 223 | 224 | try 225 | { 226 | var xml = XDocument.Parse(content); 227 | inProgressRecordingCount = xml.Descendants("MediaGrabOperation").Count(x => (string)x.Attribute("status") == "inprogress"); 228 | } 229 | catch (Exception ex) 230 | when (ex is XmlException) 231 | { 232 | OnMessageChanged($"The content could not be parsed. Reason: {ex.Message}"); 233 | return inProgressRecordingCount; 234 | } 235 | 236 | return inProgressRecordingCount; 237 | } 238 | #endregion 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Plex/AppNotInstalledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace TE.Plex 5 | { 6 | /// 7 | /// An application is not installed. 8 | /// 9 | [Serializable] 10 | public class AppNotInstalledException : Exception 11 | { 12 | public AppNotInstalledException() { } 13 | 14 | public AppNotInstalledException(string message) 15 | : base(message) { } 16 | 17 | public AppNotInstalledException( 18 | string message, 19 | Exception innerException) 20 | : base(message, innerException) { } 21 | 22 | protected AppNotInstalledException( 23 | SerializationInfo info, 24 | StreamingContext context) 25 | : base(info, context) { } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Plex/EventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace TE.Plex 8 | { 9 | public class EventSource 10 | { 11 | #region Event Delegates 12 | /// 13 | /// The delegate for the Message event handler. 14 | /// 15 | /// 16 | /// The object that triggered the event. 17 | /// 18 | /// 19 | /// The message. 20 | /// 21 | public delegate void MessageChangedEventHandler(object sender, string messagee); 22 | #endregion 23 | 24 | #region Events 25 | /// 26 | /// The MessageChanged event member. 27 | /// 28 | public event MessageChangedEventHandler MessageChanged; 29 | 30 | /// 31 | /// Triggered when the message has changed. 32 | /// 33 | protected virtual void OnMessageChanged(string message) 34 | { 35 | MessageChanged?.Invoke(this, message); 36 | } 37 | #endregion 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Plex/MediaContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | 9 | namespace TE.Plex 10 | { 11 | /// 12 | /// The MediaContaier element in the XML file. 13 | /// 14 | [XmlRoot("MediaContainer")] 15 | public class MediaContainer 16 | { 17 | /// 18 | /// The size of the current play list. 19 | /// 20 | [XmlAttribute("size")] 21 | public string Size { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Plex/PlexDataFolderNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace TE.Plex 5 | { 6 | /// 7 | /// An application is not installed. 8 | /// 9 | [Serializable] 10 | public class PlexDataFolderNotFoundException : Exception 11 | { 12 | public PlexDataFolderNotFoundException() { } 13 | 14 | public PlexDataFolderNotFoundException(string message) 15 | : base(message) { } 16 | 17 | public PlexDataFolderNotFoundException( 18 | string message, 19 | Exception innerException) 20 | : base(message, innerException) { } 21 | 22 | protected PlexDataFolderNotFoundException( 23 | SerializationInfo info, 24 | StreamingContext context) 25 | : base(info, context) { } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Plex/Registry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Win32 = Microsoft.Win32; 9 | using TE.LocalSystem; 10 | 11 | namespace TE.Plex 12 | { 13 | /// 14 | /// Contains the methods used to get the Plex data values from the Windows 15 | /// registry. 16 | /// 17 | internal class Registry : EventSource 18 | { 19 | /// 20 | /// The registry key tree for the Plex information. 21 | /// 22 | private const string RegistryPlexKey = @"SOFTWARE\Plex, Inc.\Plex Media Server\"; 23 | 24 | /// 25 | /// The registry run key that starts Plex Media Server at Windows 26 | /// startup. 27 | /// 28 | private const string RegistryRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; 29 | 30 | /// 31 | /// The registry value that starts Plex Media Server at Windows 32 | /// startup. 33 | /// 34 | private const string RegistryRunValue = "Plex Media Server"; 35 | 36 | /// 37 | /// The name of the local Plex data path registry value. 38 | /// 39 | private const string RegistryPlexDataPathValueName = "LocalAppDataPath"; 40 | 41 | /// 42 | /// The location of the Plex installation. 43 | /// 44 | private const string RegistryInstallFolder = "InstallFolder"; 45 | 46 | /// 47 | /// The user running the Plex server application. 48 | /// 49 | private WindowsUser user; 50 | 51 | /// 52 | /// The path to the Plex registry keys. 53 | /// 54 | private string plexRegistryPath; 55 | 56 | /// 57 | /// Creates an instance of the class when 58 | /// provided with the user that is running the Plex server. 59 | /// 60 | /// 61 | /// The user that is running Plex. 62 | /// 63 | /// 64 | /// The parameter is null. 65 | /// 66 | internal Registry(WindowsUser plexUser) 67 | { 68 | user = plexUser ?? throw new ArgumentNullException(nameof(plexUser)); 69 | plexRegistryPath = $"{user.Sid}\\{RegistryPlexKey}"; 70 | } 71 | 72 | /// 73 | /// Gets the value from the registry. 74 | /// 75 | /// 76 | /// The name of the value. 77 | /// 78 | /// 79 | /// The value associated with the , or null if 80 | /// name is not found. 81 | /// 82 | /// 83 | /// The parameter is null or not provided. 84 | /// 85 | /// 86 | /// The RegistryKey that contains the specified value is closed (closed keys cannot be accessed). 87 | /// 88 | /// 89 | /// The user does not have the permissions required to read from the registry key. 90 | /// 91 | /// 92 | /// The RegistryKey that contains the specified value has been marked for deletion. 93 | /// 94 | /// 95 | /// The user does not have the necessary registry rights. 96 | /// 97 | private object GetValue(string name) 98 | { 99 | if (string.IsNullOrWhiteSpace(name)) 100 | { 101 | throw new ArgumentNullException(nameof(name)); 102 | } 103 | 104 | using (Win32.RegistryKey plexRegistry = 105 | Win32.Registry.Users.OpenSubKey(plexRegistryPath)) 106 | { 107 | return plexRegistry.GetValue(name); 108 | } 109 | } 110 | 111 | /// 112 | /// Delete the Plex Server run keys for both the user that performed 113 | /// the installation, and the user associated with the Plex Service. 114 | /// 115 | /// 116 | /// True if the registry value has been deleted, false if the value 117 | /// could not be deleted. 118 | /// 119 | internal bool DeleteRunValue() 120 | { 121 | try 122 | { 123 | using (Win32.RegistryKey key = Win32.Registry.CurrentUser.OpenSubKey(RegistryRunKey, true)) 124 | { 125 | if (key != null) 126 | { 127 | try 128 | { 129 | if (key.GetValue(RegistryRunValue) == null) 130 | { 131 | return true; 132 | } 133 | 134 | key.DeleteValue(RegistryRunValue); 135 | return (key.GetValue(RegistryRunValue) == null); 136 | } 137 | catch (ArgumentException) 138 | { 139 | return true; 140 | } 141 | catch (Exception ex) 142 | when (ex is ObjectDisposedException || ex is IOException || ex is SecurityException || ex is UnauthorizedAccessException) 143 | { 144 | OnMessageChanged($"The Run key value could not be deleted. Reason: {ex.Message}"); 145 | } 146 | } 147 | } 148 | } 149 | catch (Exception ex) 150 | when (ex is ObjectDisposedException || ex is SecurityException) 151 | { 152 | OnMessageChanged($"The Run key value could not be deleted. Reason: {ex.Message}"); 153 | } 154 | 155 | return false; 156 | } 157 | 158 | /// 159 | /// Gets the location of the Plex installation folder. 160 | /// 161 | /// 162 | /// The location of the Plex installation folder, otherwise null. 163 | /// 164 | internal string GetInstallFolder() 165 | { 166 | try 167 | { 168 | // Get the Plex local data folder from the users registry hive 169 | // for the user ID associated with the Plex service 170 | return (string)GetValue(RegistryInstallFolder); 171 | } 172 | catch (Exception ex) 173 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException) 174 | { 175 | return null; 176 | } 177 | } 178 | 179 | /// 180 | /// Gets the local Plex data folder used by the Plex service. 181 | /// 182 | /// 183 | /// The full path to the local Plex data folder. 184 | /// 185 | internal string GetLocalDataFolder() 186 | { 187 | string folder; 188 | 189 | try 190 | { 191 | // Get the Plex local data folder from the users registry hive 192 | // for the user ID associated with the Plex service 193 | folder = (string)GetValue(RegistryPlexDataPathValueName); 194 | } 195 | catch (Exception ex) 196 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException) 197 | { 198 | OnMessageChanged($"The Plex local data folder could not be determined. Reason: {ex.Message}"); 199 | folder = null; 200 | } 201 | 202 | if (string.IsNullOrEmpty(folder)) 203 | { 204 | // Default to the standard local application data folder 205 | // for the Plex service user is the LocalAppDataPath value 206 | // is missing from the registry 207 | folder = user.LocalAppDataFolder; 208 | } 209 | 210 | return folder; 211 | } 212 | 213 | /// 214 | /// Gets the Plex token for the logged in Plex user. 215 | /// 216 | /// 217 | /// A Plex token or null if the token could not be retrieved. 218 | /// 219 | internal string GetToken() 220 | { 221 | try 222 | { 223 | return (string)GetValue("PlexOnlineToken"); 224 | } 225 | catch (Exception ex) 226 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException) 227 | { 228 | OnMessageChanged($"The user token could not be determined. Reason: {ex.Message}"); 229 | return null; 230 | } 231 | 232 | } 233 | 234 | /// 235 | /// Gets the update channel specified in Plex. 236 | /// 237 | /// 238 | /// An value indicating which channel to 239 | /// use to download the update. 240 | /// 241 | internal UpdateChannel GeUpdateChannel() 242 | { 243 | string value = null; 244 | try 245 | { 246 | value = (string)GetValue("ButlerUpdateChannel"); 247 | } 248 | catch (Exception ex) 249 | when (ex is ArgumentNullException || ex is ObjectDisposedException || ex is SecurityException || ex is IOException || ex is UnauthorizedAccessException) 250 | { 251 | return UpdateChannel.Public; 252 | } 253 | 254 | if (value == null) 255 | { 256 | return UpdateChannel.Public; 257 | } 258 | 259 | int updateChannel; 260 | if (!int.TryParse(value, out updateChannel)) 261 | { 262 | return UpdateChannel.Public; 263 | } 264 | 265 | return updateChannel == (int)UpdateChannel.PlexPass ? UpdateChannel.PlexPass : UpdateChannel.Public; 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Plex/ServerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using static System.Environment; 3 | using System.Linq; 4 | using System.Management; 5 | using System.ServiceProcess; 6 | using TE.LocalSystem; 7 | using System.Configuration; 8 | 9 | namespace TE.Plex 10 | { 11 | /// 12 | /// The properties and methods associated with the Plex Media Server 13 | /// service. 14 | /// 15 | public class ServerService 16 | { 17 | #region Constants 18 | /// 19 | /// The name of the Plex service. 20 | /// 21 | private static string ServiceName = ConfigurationManager.AppSettings["PlexServiceName"]; 22 | #endregion 23 | 24 | #region Properties 25 | /// 26 | /// Gets the user ID used to run the service. 27 | /// 28 | public WindowsUser LogOnUser { get; private set; } 29 | #endregion 30 | 31 | #region Constructors 32 | /// 33 | /// Creates an instance of the 34 | /// class. 35 | /// 36 | /// 37 | /// The Plex Media Server service is not installed. 38 | /// 39 | /// 40 | /// The Plex Media Server service account SID could not be found. 41 | /// 42 | public ServerService() 43 | { 44 | try 45 | { 46 | // Get the LogOnUser for the Plex Media Server service 47 | LogOnUser = GetServiceUser(); 48 | } 49 | catch (WindowsUserSidNotFound) 50 | { 51 | throw new WindowsUserSidNotFound( 52 | "The Plex Media Server service account SID could not be found."); 53 | } 54 | 55 | // If a WindowsUser object was not returned, throw an exception 56 | // indicating the service does not exist 57 | if (LogOnUser == null) 58 | { 59 | throw new ServiceNotInstalledException( 60 | "The Plex Media Server service is not installed."); 61 | } 62 | } 63 | #endregion 64 | 65 | #region Private Functions 66 | /// 67 | /// Gets the name of the Plex service log on user name. 68 | /// 69 | /// 70 | /// A object of the service 71 | /// log on user. 72 | /// 73 | private WindowsUser GetServiceUser() 74 | { 75 | Log.Write("Getting the service user."); 76 | WindowsUser user = null; 77 | 78 | if (IsInstalled()) 79 | { 80 | Log.Write("The Plex service is installed. Let's get the user associated with the service."); 81 | ManagementObject service = 82 | new ManagementObject( 83 | $"Win32_Service.Name='{ServiceName}'"); 84 | 85 | if (service == null) 86 | { 87 | Log.Write("The service user could not be found."); 88 | return null; 89 | } 90 | 91 | service.Get(); 92 | user = new WindowsUser(service["startname"].ToString().Replace( 93 | @".\", $"{MachineName}\\")); 94 | 95 | Log.Write($"The Plex service user: {user.Name}."); 96 | } 97 | else 98 | { 99 | Log.Write("The Plex service is not installed."); 100 | } 101 | 102 | return user; 103 | } 104 | #endregion 105 | 106 | #region Public Functions 107 | /// 108 | /// Checks to see if the Plex service is installed. 109 | /// 110 | /// 111 | /// True if the service is installed, false if it isn't installed. 112 | /// 113 | public static bool IsInstalled() 114 | { 115 | return ServiceController.GetServices().Any( 116 | s => s.ServiceName == ServiceName); 117 | } 118 | 119 | /// 120 | /// Stops the Plex Media Server service. 121 | /// 122 | public void Stop() 123 | { 124 | if (IsInstalled()) 125 | { 126 | using (ServiceController sc = new ServiceController(ServiceName)) 127 | { 128 | if (sc.Status == ServiceControllerStatus.Running) 129 | { 130 | sc.Stop(); 131 | sc.WaitForStatus(ServiceControllerStatus.Stopped); 132 | } 133 | } 134 | } 135 | } 136 | 137 | /// 138 | /// Starts the Plex Media Server service. 139 | /// 140 | public void Start() 141 | { 142 | if (IsInstalled()) 143 | { 144 | using (ServiceController sc = new ServiceController(ServiceName)) 145 | { 146 | sc.Start(); 147 | sc.WaitForStatus(ServiceControllerStatus.Running); 148 | } 149 | } 150 | } 151 | #endregion 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Plex/ServiceNotInstalledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace TE.Plex 5 | { 6 | /// 7 | /// A service is not installed. 8 | /// 9 | public class ServiceNotInstalledException : Exception 10 | { 11 | public ServiceNotInstalledException() { } 12 | 13 | public ServiceNotInstalledException(string message) 14 | : base(message) { } 15 | 16 | public ServiceNotInstalledException( 17 | string message, 18 | Exception innerException) 19 | : base(message, innerException) { } 20 | 21 | protected ServiceNotInstalledException( 22 | SerializationInfo info, 23 | StreamingContext context) 24 | : base(info, context) { } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Plex/SilentUpdate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using static System.Environment; 4 | using System.IO; 5 | using System.Timers; 6 | using TE.LocalSystem; 7 | 8 | namespace TE.Plex 9 | { 10 | /// 11 | /// Executes a silent Plex Media Server update. 12 | /// 13 | public class SilentUpdate 14 | { 15 | #region Constants 16 | /// 17 | /// The default wait time in seconds. 18 | /// 19 | public const int DefaultWaitTime = 30; 20 | #endregion 21 | 22 | #region Private Variables 23 | /// 24 | /// The media server object. 25 | /// 26 | private MediaServer _server = null; 27 | 28 | /// 29 | /// The wait timer. 30 | /// 31 | private Timer _timer = null; 32 | #endregion 33 | 34 | #region Properties 35 | /// 36 | /// Gets or sets the flag indicating that the update is forced to be 37 | /// installed regardless if any item is currently being played. 38 | /// 39 | public bool ForceUpdate { get; set; } = false; 40 | 41 | /// 42 | /// Gets or sets the default wait time in seconds. 43 | /// 44 | public int WaitTime { get; set; } = DefaultWaitTime; 45 | #endregion 46 | 47 | #region Constructors 48 | /// 49 | /// Initializes an instance of the class. 50 | /// 51 | /// 52 | /// Plex is not installed. 53 | /// 54 | /// 55 | /// The Plex service is not installed. 56 | /// 57 | /// 58 | /// The Windows user SID is not found. 59 | /// 60 | public SilentUpdate() 61 | { 62 | Initialize(null); 63 | } 64 | 65 | /// 66 | /// Initializes an instance of the class. 67 | /// 68 | /// 69 | /// Specifies a path to the installation log. 70 | /// 71 | /// 72 | /// Plex is not installed. 73 | /// 74 | /// 75 | /// The Plex service is not installed. 76 | /// 77 | /// 78 | /// The Windows user SID is not found. 79 | /// 80 | public SilentUpdate(string logPath) 81 | { 82 | Initialize(logPath); 83 | } 84 | #endregion 85 | 86 | #region Events 87 | /// 88 | /// Writes any messages from the Plex Media Server update to a log 89 | /// file. 90 | /// 91 | /// 92 | /// The sender object. 93 | /// 94 | /// 95 | /// The message to write to the log file. 96 | /// 97 | private void ServerUpdateMessage(object sender, string message) 98 | { 99 | Log.Write(message); 100 | } 101 | 102 | /// 103 | /// The timer has elapsed so check the play count to see if the server 104 | /// is in use. 105 | /// 106 | /// 107 | /// The sender. 108 | /// 109 | /// 112 | private void OnTimedEvent(object sender, ElapsedEventArgs e) 113 | { 114 | if (_server == null) 115 | { 116 | _timer.Enabled = false; 117 | return; 118 | } 119 | 120 | if (CheckIfCanUpdate()) 121 | { 122 | PerformUpdate(); 123 | } 124 | } 125 | #endregion 126 | 127 | #region Private Functions 128 | /// 129 | /// Checks to see if the server can be updated at this time. 130 | /// 131 | private bool CheckIfCanUpdate() 132 | { 133 | if (_server == null) 134 | { 135 | Log.Write("The server was not specified. Cannot perform the update."); 136 | _timer.Enabled = false; 137 | return false; 138 | } 139 | 140 | int playCount = _server.GetPlayCount(); 141 | int inProgressRecordingCount = _server.GetInProgressRecordingCount(); 142 | 143 | // No item is currently being played 144 | if (playCount == 0 && inProgressRecordingCount == 0) 145 | { 146 | Log.Write("The server is not in use continuing to perform the update."); 147 | _timer.Enabled = false; 148 | return true; 149 | } 150 | // At least one item is being played 151 | else if (playCount > 0 || inProgressRecordingCount > 0) 152 | { 153 | if (!ForceUpdate) 154 | { 155 | Log.Write("The server is in use. Waiting for all media and/or in progress recordings to be stopped before performing the update."); 156 | _timer.Interval = 157 | Convert.ToDouble(Math.Abs(WaitTime) * 1000); 158 | _timer.Enabled = true; 159 | return false; 160 | } 161 | else if (ForceUpdate && inProgressRecordingCount > 0) 162 | { 163 | Log.Write("The server cannot be forcefully updated while there is a recording in progress. Waiting for all in progress recordings to be stopped before performing the update."); 164 | _timer.Interval = 165 | Convert.ToDouble(Math.Abs(WaitTime) * 1000); 166 | _timer.Enabled = true; 167 | return false; 168 | } 169 | else 170 | { 171 | Log.Write("The update is set to be force. The update will continue."); 172 | _timer.Enabled = false; 173 | return true; 174 | } 175 | } 176 | // Could not determine how many items are being played 177 | else 178 | { 179 | Log.Write("The server in use status could not be determined. The server can be updated if you wish."); 180 | _timer.Enabled = false; 181 | return true; 182 | } 183 | } 184 | 185 | /// 186 | /// Initializes the properties and variables for the class. 187 | /// 188 | /// 189 | /// Plex is not installed. 190 | /// 191 | /// 192 | /// The Plex service is not installed. 193 | /// 194 | /// 195 | /// The Windows user SID is not found. 196 | /// 197 | private void Initialize(string logPath) 198 | { 199 | try 200 | { 201 | _server = new MediaServer(logPath, ServerUpdateMessage); 202 | _timer = new Timer(DefaultWaitTime * 1000); 203 | _timer.Elapsed += OnTimedEvent; 204 | _timer.Enabled = false; 205 | } 206 | catch (AppNotInstalledException) 207 | { 208 | Log.Write( 209 | "The Plex Media Server is not installed."); 210 | throw; 211 | } 212 | catch (ServiceNotInstalledException) 213 | { 214 | Log.Write( 215 | "The Plex Media Server service is not installed."); 216 | throw; 217 | } 218 | catch (WindowsUserSidNotFound ex) 219 | { 220 | Log.Write(ex.Message); 221 | throw; 222 | } 223 | catch (Exception ex) 224 | { 225 | Log.Write(ex); 226 | throw; 227 | } 228 | } 229 | 230 | /// 231 | /// Perform the server update. 232 | /// 233 | private void PerformUpdate() 234 | { 235 | try 236 | { 237 | Log.Write("Update is available"); 238 | _server.Update(); 239 | } 240 | catch (Exception ex) 241 | { 242 | Log.Write(ex.Message); 243 | Log.Write(ex.StackTrace); 244 | } 245 | } 246 | #endregion 247 | 248 | #region Public Functions 249 | /// 250 | /// Gets the value indicating that the Plex server is running. 251 | /// 252 | /// 253 | /// True if the Plex server is running, false if the server is not 254 | /// running. 255 | /// 256 | public bool IsPlexRunning() 257 | { 258 | return _server.IsRunning(); 259 | } 260 | /// 261 | /// Runs the Plex Media Server update. 262 | /// 263 | public void Run() 264 | { 265 | try 266 | { 267 | Log.Write("Checking for server update."); 268 | if (_server.IsUpdateAvailable()) 269 | { 270 | if (CheckIfCanUpdate()) 271 | { 272 | Log.Write("Update is available"); 273 | _server.Update(); 274 | } 275 | } 276 | else 277 | { 278 | Log.Write("No update is available. Exiting."); 279 | } 280 | } 281 | catch (Exception ex) 282 | { 283 | Log.Write(ex.Message); 284 | Log.Write(ex.StackTrace); 285 | } 286 | } 287 | #endregion 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /Plex/Update/CurrentVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | namespace TE.Plex.Update 9 | { 10 | /// 11 | /// The current version of Plex Media Server. 12 | /// 13 | public class CurrentVersion 14 | { 15 | /// 16 | /// Gets or sets the computer Plex Media Server releases. 17 | /// 18 | [JsonProperty("computer")] 19 | public Dictionary Computer { get; set; } 20 | 21 | /// 22 | /// Gets or sets the NAS Plex Server releases. 23 | /// 24 | [JsonProperty("nas")] 25 | public Dictionary Nas { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Plex/Update/Package.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Security.Cryptography; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | using Newtonsoft.Json; 8 | 9 | namespace TE.Plex.Update 10 | { 11 | /// 12 | /// Properties and methods for downloading the latest version of the Plex 13 | /// Media Server for Windows. 14 | /// 15 | public class Package : EventSource 16 | { 17 | #region Private Constants 18 | /// 19 | /// The public URL to the JSON data that contains information about the 20 | /// latest Plex Media Server installs. 21 | /// 22 | private const string PlexPackagePublicJsonUrl = 23 | "https://plex.tv/api/downloads/1.json"; 24 | 25 | /// 26 | /// The URL to the JSON data that contains information about the 27 | /// latest Plex Media Server installs. 28 | /// 29 | private const string PlexPackageJsonUrl = 30 | "https://plex.tv/api/downloads/5.json"; 31 | 32 | /// 33 | /// The additional querystring to add to the URL to request the Plex 34 | /// Pass edition of the Plex install. 35 | /// 36 | private const string PlexPackageJsonUrlBeta = "?channel=plexpass"; 37 | #endregion 38 | 39 | #region Private Variables 40 | /// 41 | /// The HTTP client used to connect to the Plex website. 42 | /// 43 | private HttpClient _client = new HttpClient(); 44 | 45 | /// 46 | /// The value indicating if Plex is 64-bit. 47 | /// 48 | private bool _is64Bit = false; 49 | 50 | /// 51 | /// The local application data folder for Plex. 52 | /// 53 | private string _updatesFolder; 54 | 55 | /// 56 | /// The update channel used to update Plex. 57 | /// 58 | private UpdateChannel _updateChannel; 59 | 60 | /// 61 | /// The Plex user's token. 62 | /// 63 | private string _token; 64 | #endregion 65 | 66 | #region Properties 67 | /// 68 | /// Gets the latest Windows version of the Plex Media Server. 69 | /// 70 | public SystemType LatestWindowsVersion { get; private set; } 71 | 72 | /// 73 | /// Gets the path to the installation file once it has been downloaded 74 | /// from the Plex server. This value remains null, unless the 75 | /// method is called, and the file has been downloaded successfully. 76 | /// 77 | public string FilePath { get; private set; } = null; 78 | #endregion 79 | 80 | #region Constructors 81 | /// 82 | /// Creates an instance of the class 83 | /// when provided with the Plex user's registry key and the user's token. 84 | /// 85 | /// 86 | /// The local application data folder for Plex. 87 | /// 88 | /// 89 | /// The update channel used to update Plex. 90 | /// 91 | /// 92 | /// The Plex user's token. 93 | /// 94 | /// 95 | /// An argument provided is null. 96 | /// 97 | public Package( 98 | string updatesFolder, 99 | UpdateChannel updateChannel, 100 | string token, 101 | bool is64Bit) 102 | { 103 | _updatesFolder = 104 | updatesFolder ?? throw new ArgumentNullException(nameof(updatesFolder)); 105 | _updateChannel = updateChannel; 106 | _token = token ?? throw new ArgumentNullException(nameof(token)); 107 | _is64Bit = is64Bit; 108 | } 109 | #endregion 110 | 111 | #region Private Methods 112 | /// 113 | /// Get the checksum for a specified file. 114 | /// 115 | /// 116 | /// The full path to the file. 117 | /// 118 | /// 119 | /// The checksum for the file or null if the checksum could not 120 | /// be determined. 121 | /// 122 | private string GetChecksum(string filePath) 123 | { 124 | if (string.IsNullOrEmpty(filePath)) 125 | { 126 | return null; 127 | } 128 | 129 | if (!File.Exists(filePath)) 130 | { 131 | return null; 132 | } 133 | 134 | using (SHA1 sha = SHA1.Create()) 135 | { 136 | try 137 | { 138 | using (var stream = File.OpenRead(filePath)) 139 | { 140 | byte[] hash = sha.ComputeHash(stream); 141 | 142 | if (hash == null || hash.Length == 0) 143 | { 144 | return null; 145 | } 146 | 147 | return BitConverter.ToString(hash).Replace("-", "").ToLower(); 148 | } 149 | } 150 | catch 151 | { 152 | return null; 153 | } 154 | } 155 | } 156 | 157 | /// 158 | /// Gets the filename for the latest install file. 159 | /// 160 | /// 161 | /// The filename of the latest install file or null if the latest 162 | /// install file could not be determined. 163 | /// 164 | private string GetFileName() 165 | { 166 | if (LatestWindowsVersion == null) 167 | { 168 | return null; 169 | } 170 | 171 | if (LatestWindowsVersion.Releases.Count == 0) 172 | { 173 | OnMessageChanged("WARN: There were no releases specified from Plex for Windows."); 174 | return null; 175 | } 176 | 177 | if (LatestWindowsVersion.Releases[0] == null) 178 | { 179 | OnMessageChanged("WARN: There were no releases specified from Plex for Windows."); 180 | return null; 181 | } 182 | 183 | string url = LatestWindowsVersion.GetUrl(_is64Bit); 184 | if (string.IsNullOrEmpty(url)) 185 | { 186 | OnMessageChanged("WARN: The URL for the Windows release was not specified."); 187 | return null; 188 | } 189 | 190 | return Path.GetFileName(url); 191 | } 192 | 193 | /// 194 | /// Gets the full local path for the install package. 195 | /// 196 | /// 197 | /// The full path for the install package, or null if the full 198 | /// path could not be determined. 199 | /// 200 | private string GetFullPath() 201 | { 202 | if (LatestWindowsVersion == null) 203 | { 204 | return null; 205 | } 206 | 207 | // Get the local path for the latest install and verify the folder 208 | // exists 209 | string folder = GetPath(); 210 | if (string.IsNullOrEmpty(folder)) 211 | { 212 | return null; 213 | } 214 | 215 | // Get the full path to the latest install and then verify the file 216 | // exists 217 | string name = GetFileName(); 218 | if (string.IsNullOrEmpty(name)) 219 | { 220 | return null; 221 | } 222 | 223 | return Path.Combine(folder, name); 224 | } 225 | 226 | /// 227 | /// Gets the download package local path. 228 | /// 229 | /// 230 | /// The downloaded package local path or null if the path could not 231 | /// be determined. 232 | /// 233 | private string GetPath() 234 | { 235 | try 236 | { 237 | if (string.IsNullOrWhiteSpace(_updatesFolder)) 238 | { 239 | return null; 240 | } 241 | 242 | if (LatestWindowsVersion == null) 243 | { 244 | return null; 245 | } 246 | 247 | if (string.IsNullOrWhiteSpace(LatestWindowsVersion.Version)) 248 | { 249 | return null; 250 | } 251 | 252 | return Path.Combine( 253 | _updatesFolder, 254 | $@"{LatestWindowsVersion.Version}\packages"); 255 | } 256 | catch (Exception ex) 257 | when (ex is ArgumentException || ex is System.Security.SecurityException) 258 | { 259 | return null; 260 | } 261 | } 262 | 263 | /// 264 | /// Gets the URL for the specified update channel that is specified 265 | /// on the Plex server. 266 | /// 267 | /// 268 | /// The URL for the specified update channel package, or the public URL 269 | /// if the URL could not be determined. 270 | /// 271 | private string GetUrl() 272 | { 273 | if (_updateChannel == UpdateChannel.PlexPass) 274 | { 275 | OnMessageChanged("The update channel is set for Plex Pass."); 276 | return PlexPackageJsonUrl + PlexPackageJsonUrlBeta; 277 | } 278 | else 279 | { 280 | OnMessageChanged("The update channel is set for public."); 281 | return PlexPackagePublicJsonUrl; 282 | } 283 | } 284 | 285 | /// 286 | /// Gets the JSON string value for the latest versions of Plex from 287 | /// the Plex download site. 288 | /// 289 | /// 290 | /// The JSON string value if the request was successful, or null if 291 | /// the request was not successful. 292 | /// 293 | private string GetJson() 294 | { 295 | string content; 296 | 297 | if (_token != null) 298 | { 299 | _client.DefaultRequestHeaders.Add("X-Plex-Token", _token); 300 | } 301 | 302 | try 303 | { 304 | // Get the URL for the package specified by the update channel 305 | // set in the Plex server 306 | string url = GetUrl(); 307 | if (url == null) 308 | { 309 | return null; 310 | } 311 | 312 | OnMessageChanged($"Sending request to Plex: {url}"); 313 | using (HttpResponseMessage response = _client.GetAsync(url).Result) 314 | { 315 | content = response.Content.ReadAsStringAsync().Result; 316 | } 317 | 318 | return content; 319 | } 320 | catch (HttpRequestException ex) 321 | { 322 | OnMessageChanged($"Could not get Plex package information. Message: {ex.Message}"); 323 | return null; 324 | } 325 | catch (AggregateException ae) 326 | { 327 | foreach (var e in ae.Flatten().InnerExceptions) 328 | { 329 | OnMessageChanged($"Could not get Plex package information. Message: {e.Message}"); 330 | 331 | if (e.InnerException != null) 332 | { 333 | OnMessageChanged($"Additional information: {e.InnerException.Message}"); 334 | } 335 | } 336 | return null; 337 | } 338 | } 339 | 340 | /// 341 | /// Initialize the latest available of Plex Media Server to download. 342 | /// 343 | private void Initialize() 344 | { 345 | OnMessageChanged("Checking for the latest version from Plex."); 346 | string json = GetJson(); 347 | 348 | if (string.IsNullOrEmpty(json)) 349 | { 350 | OnMessageChanged("Could not get the latest version information from Plex."); 351 | return; 352 | } 353 | 354 | OnMessageChanged("Parsing the information from Plex."); 355 | CurrentVersion versions = 356 | JsonConvert.DeserializeObject(json); 357 | 358 | LatestWindowsVersion = versions.Computer["Windows"]; 359 | if (LatestWindowsVersion == null) 360 | { 361 | OnMessageChanged("Could not get the latest version information from Plex."); 362 | return; 363 | } 364 | } 365 | #endregion 366 | 367 | #region Public Methods 368 | /// 369 | /// Downloads the latest Plex Media Server installation file for 370 | /// Windows. 371 | /// 372 | /// 373 | /// The URL for the installation. 374 | /// 375 | /// 376 | /// The full path where the file is to be saved. 377 | /// 378 | /// 379 | /// A of the download. 380 | /// 381 | /// 382 | /// If the file has already been downloaded, and the file is valid, 383 | /// then the file won't be downloaded again. 384 | /// 385 | public async Task Download() 386 | { 387 | OnMessageChanged("Getting ready to download the latest package."); 388 | 389 | if (LatestWindowsVersion == null) 390 | { 391 | Initialize(); 392 | if (LatestWindowsVersion == null) 393 | { 394 | OnMessageChanged( 395 | "The latest Windows version has not been specified."); 396 | return false; 397 | } 398 | } 399 | 400 | // Verify that the URL for the latest release has been stored 401 | string url = LatestWindowsVersion.GetUrl(_is64Bit); 402 | if (string.IsNullOrEmpty(url)) 403 | { 404 | OnMessageChanged( 405 | "The URL for the latest version was not specified."); 406 | return false; 407 | } 408 | 409 | FilePath = GetFullPath(); 410 | if (string.IsNullOrEmpty(FilePath)) 411 | { 412 | OnMessageChanged( 413 | "The path to the local downloaded install file could not be determined."); 414 | return false; 415 | } 416 | 417 | string directory = Path.GetDirectoryName(FilePath); 418 | if (!Directory.Exists(directory)) 419 | { 420 | try 421 | { 422 | OnMessageChanged($"Creating folder: {directory}."); 423 | Directory.CreateDirectory(directory); 424 | } 425 | catch (Exception ex) 426 | { 427 | OnMessageChanged($"Could not create download folder. Reason: {ex.Message}"); 428 | return false; 429 | } 430 | } 431 | else 432 | { 433 | // If the directory already exists, check to see if the file 434 | // also exists 435 | if (File.Exists(FilePath)) 436 | { 437 | OnMessageChanged($"The file, {FilePath}, exists. Checking to see if the package is valid."); 438 | // If the file is valid - meaning the checksum matches the 439 | // checksum of the file to be downloaded, then return true 440 | // to avoid redownloading the same file a second time 441 | if (IsValid()) 442 | { 443 | OnMessageChanged("Since the package is valid - not downloading again."); 444 | return true; 445 | } 446 | } 447 | } 448 | 449 | try 450 | { 451 | OnMessageChanged("Downloading the latest installation package from Plex."); 452 | // Get the response once it is available and the headers are read 453 | using (HttpResponseMessage response = 454 | await _client.GetAsync( 455 | url, 456 | HttpCompletionOption.ResponseHeadersRead).ConfigureAwait( 457 | false)) 458 | { 459 | // Get the stream content 460 | using (Stream streamToReadFrom = 461 | await response.Content.ReadAsStreamAsync()) 462 | { 463 | // Write the stream to the local file path 464 | using (Stream streamToWriteTo = 465 | File.Open(FilePath, FileMode.Create)) 466 | { 467 | await streamToReadFrom.CopyToAsync(streamToWriteTo); 468 | } 469 | } 470 | } 471 | } 472 | catch (Exception ex) 473 | when (ex is HttpRequestException || ex is IOException || ex is UnauthorizedAccessException || ex is NotSupportedException) 474 | { 475 | OnMessageChanged($"Could not download update. Message: {ex.Message}."); 476 | return false; 477 | } 478 | 479 | // Check to see if the downloaded file is valid 480 | return IsValid(); 481 | } 482 | 483 | /// 484 | /// Converts the string value of the downloaded file version into 485 | /// a object. 486 | /// 487 | /// 488 | /// A object that represents the version of 489 | /// the downloaded file. 490 | /// 491 | public Version GetVersion() 492 | { 493 | if (LatestWindowsVersion == null) 494 | { 495 | Initialize(); 496 | if (LatestWindowsVersion == null) 497 | { 498 | return default; 499 | } 500 | } 501 | 502 | string version = LatestWindowsVersion.Version; 503 | if (string.IsNullOrEmpty(version)) 504 | { 505 | return default; 506 | } 507 | 508 | // The regular expression used to parse the file version 509 | Regex regEx = new Regex( 510 | @"^(?\d+)\.(?\d+)\.(?\d+)\.(?\d+)\-\S+$"); 511 | 512 | try 513 | { 514 | // Ensure that a match is made 515 | if (regEx.IsMatch(version)) 516 | { 517 | // Find the first match for the regular expression in the value 518 | Match match = regEx.Match(version); 519 | 520 | // Return the version object 521 | Version fileVersion = new Version( 522 | Convert.ToInt32(match.Groups["Major"].Value), 523 | Convert.ToInt32(match.Groups["Minor"].Value), 524 | Convert.ToInt32(match.Groups["Build"].Value), 525 | Convert.ToInt32(match.Groups["Revision"].Value)); 526 | 527 | OnMessageChanged( 528 | $"The latest file version available for download is {fileVersion.ToString()}."); 529 | 530 | return fileVersion; 531 | } 532 | else 533 | { 534 | return default; 535 | } 536 | } 537 | catch (Exception ex) 538 | when (ex is ArgumentOutOfRangeException || ex is RegexMatchTimeoutException) 539 | { 540 | return default; 541 | } 542 | } 543 | 544 | /// 545 | /// Checks to see if the downloaded install package is a valid package. 546 | /// 547 | /// 548 | public bool IsValid() 549 | { 550 | if (LatestWindowsVersion == null) 551 | { 552 | Initialize(); 553 | if (LatestWindowsVersion == null) 554 | { 555 | OnMessageChanged( 556 | "The package is not valid. Could not get the latest version information from Plex."); 557 | return false; 558 | } 559 | } 560 | 561 | // Get the local path for the latest install and verify the folder 562 | // exists 563 | string folder = GetPath(); 564 | if (string.IsNullOrEmpty(folder)) 565 | { 566 | OnMessageChanged("The package folder could not be determined."); 567 | return false; 568 | } 569 | 570 | if (!Directory.Exists(folder)) 571 | { 572 | OnMessageChanged( 573 | $"The package is not valid. The folder, {folder}, does not exist."); 574 | return false; 575 | } 576 | 577 | // Get the full path to the latest install and then verify the file 578 | // exists 579 | string name = GetFileName(); 580 | if (string.IsNullOrEmpty(name)) 581 | { 582 | OnMessageChanged( 583 | $"The package is not valid. Could not get the file name {name}."); 584 | return false; 585 | } 586 | 587 | string filePath = Path.Combine(folder, name); 588 | if (!File.Exists(filePath)) 589 | { 590 | OnMessageChanged( 591 | $"The package is not valid. Could not find {filePath}."); 592 | return false; 593 | } 594 | 595 | 596 | // Get the checksum for the latest install and the validate that 597 | // the checksum matches the checksum from the Plex site 598 | string checksum = GetChecksum(filePath); 599 | 600 | OnMessageChanged("Checking if the installation package is valid."); 601 | bool isValid = 602 | checksum.Equals(LatestWindowsVersion.GetCheckSum(_is64Bit)); 603 | 604 | if (isValid) 605 | { 606 | OnMessageChanged("The package is valid. The checksums match."); 607 | } 608 | else 609 | { 610 | OnMessageChanged("The package is not valid. The checksums match."); 611 | } 612 | return isValid; 613 | } 614 | 615 | /// 616 | /// Returns the string representation for this object. 617 | /// 618 | /// 619 | /// The string value. 620 | /// 621 | public override string ToString() 622 | { 623 | return FilePath; 624 | } 625 | #endregion 626 | } 627 | } 628 | -------------------------------------------------------------------------------- /Plex/Update/Release.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | namespace TE.Plex.Update 9 | { 10 | /// 11 | /// A release of the Plex Media Server. 12 | /// 13 | public class Release 14 | { 15 | /// 16 | /// The label associated with the release. 17 | /// 18 | [JsonProperty("label")] 19 | public string Label { get; set; } 20 | 21 | /// 22 | /// The name of the release. 23 | /// 24 | [JsonProperty("build")] 25 | public string Build { get; set; } 26 | 27 | /// 28 | /// The distribution of the release. 29 | /// 30 | [JsonProperty("distro")] 31 | public string Distro { get; set; } 32 | 33 | /// 34 | /// The download URL for the build. 35 | /// 36 | [JsonProperty("url")] 37 | public string Url { get; set; } 38 | 39 | /// 40 | /// The checksum of the build. 41 | /// 42 | [JsonProperty("checksum")] 43 | public string CheckSum { get; set; } 44 | 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Plex/Update/SystemType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | namespace TE.Plex.Update 9 | { 10 | /// 11 | /// Information about the release for a specified system type. 12 | /// 13 | public class SystemType 14 | { 15 | /// 16 | /// Name of the 32-bit build. 17 | /// 18 | private const string BUILD32BIT = "windows-x86"; 19 | /// 20 | /// Name of the 64-bit build. 21 | /// 22 | private const string BUILD64BIT = "windows-x86_64"; 23 | 24 | /// 25 | /// The ID of the system type. 26 | /// 27 | [JsonProperty("id")] 28 | public string Id { get; set; } 29 | 30 | /// 31 | /// The name of the system type. 32 | /// 33 | [JsonProperty("name")] 34 | public string Name { get; set; } 35 | 36 | /// 37 | /// The release date of the Plex Media Server. 38 | /// 39 | [JsonProperty("release_date")] 40 | public string ReleaseDate { get; set; } 41 | 42 | /// 43 | /// The version number of the Plex Media Server. 44 | /// 45 | [JsonProperty("version")] 46 | public string Version { get; set; } 47 | 48 | /// 49 | /// The URL to the requirements of the Plex Media Server. 50 | /// 51 | [JsonProperty("requirements")] 52 | public string Requirements { get; set; } 53 | 54 | /// 55 | /// Any additional information associated with this verison of the Plex 56 | /// Media Server. 57 | /// 58 | [JsonProperty("extra_info")] 59 | public string ExtraInfo { get; set; } 60 | 61 | /// 62 | /// A list of items added to this version of the Plex Media Server. 63 | /// 64 | [JsonProperty("items_added")] 65 | public string ItemsAdded { get; set; } 66 | 67 | /// 68 | /// A list of items that were fixed with this version of the Plex Media 69 | /// Server. 70 | /// 71 | [JsonProperty("items_fixed")] 72 | public string ItemsFixed { get; set; } 73 | 74 | /// 75 | /// A object of objects for 76 | /// each release of the Plex Media Server for this system type. 77 | /// 78 | [JsonProperty("releases")] 79 | public List Releases { get; set; } = new List(); 80 | 81 | /// 82 | /// Gets the download URL based on whether the 32-bit or 64-bit 83 | /// version of Plex Media Server is to be downloaded. 84 | /// 85 | /// 86 | /// Flag indicating which version of Plex Media Server is installed. 87 | /// 88 | /// 89 | /// The URL for the version of Plex Media Server, otherwise null. 90 | /// 91 | public string GetUrl(bool is64Bit) 92 | { 93 | foreach (Release release in Releases) 94 | { 95 | if (release.Build.Equals(BUILD32BIT, StringComparison.OrdinalIgnoreCase) 96 | && !is64Bit) 97 | { 98 | return release.Url; 99 | } 100 | 101 | if (release.Build.Equals(BUILD64BIT, StringComparison.OrdinalIgnoreCase) 102 | && is64Bit) 103 | { 104 | return release.Url; 105 | } 106 | } 107 | 108 | return null; 109 | } 110 | 111 | /// 112 | /// Gets the checksum based on whether the 32-bit or 64-bit version of 113 | /// Plex Media Server is to be downloaded. 114 | /// 115 | /// 116 | /// Flag indicating which version of Plex Media Server is installed. 117 | /// 118 | /// 119 | /// The checksum for the installation file of Plex Media Server, 120 | /// otherwise null. 121 | /// 122 | public string GetCheckSum(bool is64Bit) 123 | { 124 | foreach (Release release in Releases) 125 | { 126 | if (release.Build.Equals(BUILD32BIT, StringComparison.OrdinalIgnoreCase) 127 | && !is64Bit) 128 | { 129 | return release.CheckSum; 130 | } 131 | 132 | if (release.Build.Equals(BUILD64BIT, StringComparison.OrdinalIgnoreCase) 133 | && is64Bit) 134 | { 135 | return release.CheckSum; 136 | } 137 | } 138 | 139 | return null; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /PlexServerAutoUpdater.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | {5E327EE8-620A-4945-81CE-029CE9448171} 5 | {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 6 | Debug 7 | AnyCPU 8 | WinExe 9 | TE 10 | psupdate 11 | v4.8 12 | Properties 13 | False 14 | False 15 | False 16 | OnBuildSuccess 17 | False 18 | False 19 | False 20 | obj\$(Configuration)\ 21 | 4 22 | 23 | 24 | 25 | x86 26 | 4194304 27 | False 28 | Auto 29 | 4096 30 | 31 | 32 | bin\Debug\ 33 | True 34 | Full 35 | False 36 | True 37 | DEBUG;TRACE 38 | obj\ 39 | Project 40 | 41 | 42 | bin\Release\ 43 | False 44 | None 45 | True 46 | False 47 | TRACE 48 | obj\ 49 | 50 | 51 | false 52 | 53 | 54 | false 55 | 56 | 57 | 58 | ..\..\Visual Studio 2017\Projects\Supporting\Newtonsoft.Json\net45\Newtonsoft.Json.dll 59 | 60 | 61 | 62 | 63 | 3.5 64 | 65 | 66 | 67 | 3.5 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 3.5 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Form 104 | 105 | 106 | MainForm.cs 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | MainForm.cs 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /PlexServerAutoUpdater.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | # SharpDevelop 5.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexServerAutoUpdater", "PlexServerAutoUpdater.csproj", "{5E327EE8-620A-4945-81CE-029CE9448171}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {5E327EE8-620A-4945-81CE-029CE9448171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {5E327EE8-620A-4945-81CE-029CE9448171}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {5E327EE8-620A-4945-81CE-029CE9448171}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {5E327EE8-620A-4945-81CE-029CE9448171}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | EndGlobal 19 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using static System.Console; 4 | using static System.Environment; 5 | using System.Runtime.InteropServices; 6 | using System.Windows.Forms; 7 | using TE.LocalSystem; 8 | using TE; 9 | using static TE.SystemExitCodes; 10 | 11 | namespace TE.Plex 12 | { 13 | /// 14 | /// Class with program entry point. 15 | /// 16 | internal sealed class Program 17 | { 18 | /// 19 | /// The parent process. 20 | /// 21 | private const int ATTACH_PARENT_PROCESS = -1; 22 | 23 | /// 24 | /// Attach to the console window. 25 | /// 26 | /// 27 | /// The ID of the process. 28 | /// 29 | /// 30 | /// True if successful, false if not successful. 31 | /// 32 | [DllImport("kernel32.dll")] 33 | static extern bool AttachConsole(int dwProcessId); 34 | 35 | /// 36 | /// Program entry point. 37 | /// 38 | [STAThread] 39 | private static int Main(string[] args) 40 | { 41 | // redirect console output to parent process; 42 | // must be before any calls to Console.WriteLine() 43 | AttachConsole(ATTACH_PARENT_PROCESS); 44 | 45 | Arguments arguments = new Arguments(args); 46 | 47 | 48 | bool isSilent = (arguments["silent"] != null); 49 | string logPath = arguments["log"]; 50 | 51 | Log.SetFolder(logPath); 52 | Log.Delete(); 53 | 54 | try 55 | { 56 | Log.Write("Getting windows user."); 57 | WindowsUser user = new WindowsUser(); 58 | 59 | Log.Write("Checking if user is an administrator."); 60 | // Check if the user running this application is an administrator 61 | if (!user.IsAdministrator()) 62 | { 63 | string message = "This application must be run from an administrative account."; 64 | 65 | if (!isSilent) 66 | { 67 | // If the user is not an administrator, then exit 68 | MessageBox.Show( 69 | message, 70 | "Plex Server Updater", 71 | MessageBoxButtons.OK, 72 | MessageBoxIcon.Stop); 73 | } 74 | 75 | Log.Write(message); 76 | 77 | ExitCode = ERROR_ACCESS_DENIED; 78 | return ERROR_ACCESS_DENIED; 79 | } 80 | } 81 | catch (Exception ex) 82 | { 83 | if (!isSilent) 84 | { 85 | MessageBox.Show( 86 | ex.Message, 87 | "Plex Server Updater", 88 | MessageBoxButtons.OK, 89 | MessageBoxIcon.Stop); 90 | } 91 | 92 | Log.Write(ex); 93 | Log.Write(ex.StackTrace); 94 | 95 | ExitCode = 1; 96 | return 1; 97 | } 98 | 99 | if (isSilent) 100 | { 101 | try 102 | { 103 | bool isForceUpdate = (arguments["force"] != null); 104 | 105 | int waitTime = SilentUpdate.DefaultWaitTime; 106 | if (arguments["wait"] != null) 107 | { 108 | if (!Int32.TryParse(arguments["wait"], out waitTime)) 109 | { 110 | waitTime = SilentUpdate.DefaultWaitTime; 111 | } 112 | } 113 | 114 | // Run the update silently 115 | Log.Write("Initializing the silent update."); 116 | SilentUpdate silentUpdate = new SilentUpdate(Log.Folder); 117 | silentUpdate.ForceUpdate = isForceUpdate; 118 | silentUpdate.WaitTime = waitTime; 119 | silentUpdate.Run(); 120 | 121 | if (silentUpdate.IsPlexRunning()) 122 | { 123 | ExitCode = ERROR_SUCCESS; 124 | return ERROR_SUCCESS; 125 | } 126 | else 127 | { 128 | ExitCode = 1; 129 | return 1; 130 | } 131 | } 132 | catch (Exception ex) 133 | { 134 | Log.Write(ex.Message); 135 | Log.Write(ex.StackTrace); 136 | 137 | ExitCode = 1; 138 | return 1; 139 | } 140 | } 141 | else 142 | { 143 | try 144 | { 145 | // Display the main form 146 | Application.EnableVisualStyles(); 147 | Application.SetCompatibleTextRenderingDefault(false); 148 | 149 | Log.Write("Initializing the update window."); 150 | MainForm mainForm = new MainForm(); 151 | 152 | // Check to see if the form is disposed becase there was an 153 | // issue with initializing the form 154 | if (!mainForm.IsDisposed) 155 | { 156 | Log.Write("Displaying the update window."); 157 | Application.Run(mainForm); 158 | } 159 | 160 | ExitCode = ERROR_SUCCESS; 161 | return ERROR_SUCCESS; 162 | } 163 | catch (Exception ex) 164 | { 165 | MessageBox.Show( 166 | ex.Message, 167 | "Plex Server Updater", 168 | MessageBoxButtons.OK, 169 | MessageBoxIcon.Stop); 170 | 171 | Log.Write(ex); 172 | Log.Write(ex.StackTrace); 173 | 174 | ExitCode = 1; 175 | return 1; 176 | } 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | #region Using directives 2 | using System; 3 | using System.Reflection; 4 | using System.Runtime.InteropServices; 5 | 6 | #endregion 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle ("PlexServerAutoUpdater")] 11 | [assembly: AssemblyDescription("Updates the Plex Server when it is run as a service.")] 12 | [assembly: AssemblyConfiguration ("")] 13 | [assembly: AssemblyCompany ("")] 14 | [assembly: AssemblyProduct ("PlexServerAutoUpdater")] 15 | [assembly: AssemblyCopyright("Copyright 2021")] 16 | [assembly: AssemblyTrademark ("")] 17 | [assembly: AssemblyCulture ("")] 18 | // This sets the default COM visibility of types in the assembly to invisible. 19 | // If you need to expose a type to COM, use [ComVisible(true)] on that type. 20 | [assembly: ComVisible (false)] 21 | // The assembly version has following format : 22 | // 23 | // Major.Minor.Build.Revision 24 | // 25 | // You can specify all the values or you can use the default the Revision and 26 | // Build Numbers by using the '*' as shown below: 27 | [assembly: AssemblyVersion("0.2.1.1")] 28 | [assembly: AssemblyFileVersion("0.2.1.1")] 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plex Server Auto Updater 2 | 3 | The Plex Server Auto Updater application allows the Plex Media server to be updated automatically when it is [run as a Windows service]. 4 | 5 | ## What does it do? 6 | When the Plex Server Auto Updater performs an update, the following tasks are done: 7 | 8 | - Downloads and verifies the latest update. 9 | - Stops the Plex service. 10 | - Stops any Plex processes that are running. 11 | - Installs the latest update. 12 | - Deletes the Run keys from the registry to prevent Plex from running outside of the service. 13 | - Stops any Plex processes that are running after the update. 14 | - Restarts the Plex service. 15 | 16 | ## Installation 17 | The auto updater is easy to install, in fact, there isn't an install. It is a portable application and can be run from anywhere on the machine that has the Plex service installed. 18 | 19 | To use the Plex Server Auto Updater, use the following steps: 20 | 21 | - Download the [latest release]. 22 | - Extract the psupdate.exe from the zip file into any directory. 23 | - Double-click the executable and click the "Update" button to update the Plex Media Server. 24 | 25 | ## Scheduling a silent, automatic update 26 | The Plex Server Auto Updater can be run silently from any commandline using the following: 27 | 28 | psupdate.exe -silent 29 | 30 | The easiest way to keep Plex Media Server updated is to schedule the Plex Server Auto Updater from the Windows task scheduler. You can find information about how to do this from the [How to Update Plex Automatically When Run as a Service] post on [Technically Easy] or [Updating Plex When Plex is Running as a Windows Service] on [Plexopedia]. 31 | 32 | Of course, you can use any scheduling application with Plex Server Auto Updater by running the psupdate.exe with the -silent argument. 33 | 34 | ## Waiting for streaming to complete 35 | By default, the updater will only update the Plex server if there is no client streaming media. If there is a client streaming from the Plex server, the update will wait until the server is free. 36 | 37 | You have a few options on how Plex is updated when media is streaming: 38 | 39 | 1. Leave the default and the updater will wait and then check the server every 30 seconds to see if the streaming has completed before performing the update. 40 | 2. From the GUI, uncheck the "Only update when not in use" checkbox, and then allow the update the go ahead regardless if Plex is streaming media. 41 | 3. You can specify the "-wait [seconds]" argument to specify how many seconds the updater will wait to check to see if the streaming as completed. 42 | 4. When running the update silently (using the -silent parameter), you can specify the -force parameter to force the update. 43 | 44 | ## Log File Location 45 | The default log location is: %TEMP%\plex-updater.txt. Any installation log files are also stored in %TEMP%. 46 | 47 | If the -log parameter is specified on the command line with a valid directory, then that directory will be used to store all the log files. 48 | 49 | [run as a Windows service]: https://forums.plex.tv/discussion/93994/pms-as-a-service/ 50 | [latest release]: https://github.com/TechieGuy12/PlexServerAutoUpdater/releases/latest 51 | [How to Update Plex Automatically When Run as a Service]: http://technicallyeasy.net/2016/03/update-plex-automatically-running-plex-service/ 52 | [Technically Easy]: http://technicallyeasy.net 53 | [Updating Plex When Plex is Running as a Windows Service]: https://www.plexopedia.com/plex-media-server/windows/updating-plex-media-server-service/ 54 | [Plexopedia]: https://www.plexopedia.com 55 | -------------------------------------------------------------------------------- /app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------