├── dump.cmd
├── Properties
└── launchSettings.json
├── VitaDB.sln
├── VitaDB.csproj
├── VitaDB.ini
├── .gitattributes
├── Sfo.cs
├── .gitignore
├── Settings.cs
├── Database.cs
├── Rif.cs
├── README.md
├── Update.cs
├── App.cs
├── Json.cs
├── Utilities.cs
├── Pkg.cs
└── LICENSE
/dump.cmd:
--------------------------------------------------------------------------------
1 | dotnet bin\Release\net5.0\VitaDB.dll -m
2 | dotnet bin\Release\net5.0\VitaDB.dll -d
3 |
--------------------------------------------------------------------------------
/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "VitaDB": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--wait-for-key",
6 | "workingDirectory": "$(SolutionDir)"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/VitaDB.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26730.16
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VitaDB", "VitaDB.csproj", "{817B273C-6B4C-4BB0-BBF5-0CC8F865F479}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {817B273C-6B4C-4BB0-BBF5-0CC8F865F479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {817B273C-6B4C-4BB0-BBF5-0CC8F865F479}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {817B273C-6B4C-4BB0-BBF5-0CC8F865F479}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {817B273C-6B4C-4BB0-BBF5-0CC8F865F479}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {922102DA-2F2A-4681-87A5-240A662EA2E0}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/VitaDB.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net5.0
6 | 0.9
7 | VitaSmith
8 | © 2017-2019 VitaSmith
9 | https://www.gnu.org/licenses/gpl-3.0.en.html
10 | https://github.com/VitaSmith/VitaDB
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/VitaDB.ini:
--------------------------------------------------------------------------------
1 | ; VitaDB INI file
2 | ; Note: This UTF-8 file should be saved with a BOM
3 |
4 | [db]
5 | name = VitaDB.db
6 | local_sql = VitaDB.sql
7 | remote_sql = https://raw.githubusercontent.com/VitaSmith/VitaDB/master/VitaDB.sql
8 | local_cache = PkgCache.json
9 | remote_cache = https://raw.githubusercontent.com/VitaSmith/VitaDB/master/PkgCache.json
10 |
11 | [csv]
12 | ;separator = ,
13 | nps_app = http://nopaystation.com/tsv/PSV_GAMES.tsv
14 | nps_demo = http://nopaystation.com/tsv/PSV_DEMOS.tsv
15 | nps_dlc = http://nopaystation.com/tsv/PSV_DLCS.tsv
16 | nps_theme = http://nopaystation.com/tsv/PSV_THEMES.tsv
17 | nps_psm = http://nopaystation.com/tsv/PSM_GAMES.tsv
18 | nps_psp = http://nopaystation.com/tsv/PSP_GAMES.tsv
19 | nps_xlsx = https://docs.google.com/spreadsheets/d/18HHxaQhGgqDjH2mIgC3T1OVWNRr_4HHfuLsYmiKXnfk/export?format=xlsx
20 |
21 | [csv_mapping]
22 | TITLE_ID = Title ID
23 | NAME = Name
24 | PKG_URL = PKG direct link
25 | ZRIF = zRIF
26 |
27 | [regions]
28 | PCSA = USA
29 | PCSB = EUR
30 | PCSC = JPN
31 | PCSD = ASN
32 | PCSE = USA
33 | PCSF = EUR
34 | PCSG = JPN
35 | PCSH = ASN
36 | NPNA = USA
37 | NPOA = EUR
38 | NPPA = JAP
39 | NPQA = ASN
40 | ;PCSI = DEV
41 | ;VCAS = ASN
42 | ;VCJS = JPN
43 | ;VLAS = ASN
44 | ;VLJM = JPN
45 | ;VLJS = JPN
46 |
47 | ; languages to be used when fetching content from the PSN store
48 | [languages]
49 | PCSA = en-us
50 | PCSB = en-ie
51 | PCSC = ja-jp
52 | PCSD = en-hk
53 | PCSE = en-us
54 | PCSF = en-ie
55 | PCSG = ja-jp
56 | PCSH = en-hk
57 | PCSI = en-us
58 | NPNA = en-us
59 | NPOA = en-ie
60 | NPPA = ja-jp
61 | NPQA = en-hk
62 | VCAS = en-hk
63 | VCJS = ja-jp
64 | VLAS = en-hk
65 | VLJM = ja-jp
66 | VLJS = ja-jp
67 |
68 | [limits]
69 | ; Single region to process, or ALL to for all regions
70 | region = ALL
71 | ; Range of IDs to process for each region
72 | range = 1,1300
73 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Sfo.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using System;
11 |
12 | using static VitaDB.Utilities;
13 |
14 | namespace VitaDB
15 | {
16 | public class Sfo
17 | {
18 | public string Category;
19 | public UInt32? AppVer;
20 | public UInt32? SysVer;
21 | public UInt32? CDate;
22 |
23 | static readonly byte[] sfo_magic = { 0x00, (byte)'P', (byte)'S', (byte)'F' };
24 |
25 | ///
26 | /// Generic method to get the value associated with a key from SFO data.
27 | ///
28 | /// The type of the value to be returned.
29 | /// The SFO data.
30 | /// The key.
31 | /// The value associated with they key.
32 | public static T GetValue(byte[] sfo, string key)
33 | {
34 | UInt32 keys = GetLe32(sfo, 0x08);
35 | UInt32 values = GetLe32(sfo, 0x0c);
36 | UInt32 count = GetLe32(sfo, 0x10);
37 | UInt32 value_length = 0;
38 | UInt16 value_type = 0;
39 |
40 | int i;
41 | for (i = 0; i < count; i++)
42 | {
43 | if (MemCpyStr(sfo, (int)keys + GetLe16(sfo, i * 16 + 20)) == key)
44 | {
45 | value_type = GetLe16(sfo, 0x14 + (i * 0x10) + 2);
46 | value_length = GetLe32(sfo, 0x14 + (i * 0x10) + 4);
47 | break;
48 | }
49 | }
50 | if (i >= count)
51 | return default(T);
52 |
53 | int pos = (int)(values + GetLe32(sfo, i * 16 + 20 + 12));
54 | if (typeof(T) == typeof(string))
55 | {
56 | string value;
57 | switch (value_type)
58 | {
59 | case 0x0204:
60 | // NUL terminated string
61 | value = MemCpyStr(sfo, pos);
62 | if (value.Length != value_length - 1)
63 | Console.Error.WriteLine($"[WARNING] SFO: Unexpected length {value.Length}, while looking for NUL-terminated key '{key}'");
64 | break;
65 | case 0x0004:
66 | // non-NUL terminated
67 | value = MemCpyStr(sfo, pos, (int)value_length);
68 | break;
69 | default:
70 | throw new ApplicationException($"SFO: Unexpected type 0x{value_type:X4}, while looking for string key '{key}'.");
71 | }
72 | return (T)Convert.ChangeType(value, typeof(T));
73 | }
74 | else if (typeof(T) == typeof(UInt32))
75 | {
76 | if (value_type != 0x0404)
77 | throw new ApplicationException($"SFO: Unexpected type 0x{value_type:X4}, while looking for integer key '{key}'.");
78 | if (value_length != 4)
79 | throw new ApplicationException($"SFO: Unexpected length {value_length}, while looking for for integer key '{key}'.");
80 | var value = GetLe32(sfo, pos);
81 | return (T)Convert.ChangeType(value, typeof(T));
82 | }
83 | else
84 | {
85 | throw new ApplicationException($"SFO: Unhandled type '{typeof(T)}' requested for key {key}");
86 | }
87 | }
88 |
89 | public override string ToString()
90 | {
91 | string str = "(Sfo Object)\n";
92 | foreach (var attr in typeof(Sfo).GetFields())
93 | str += ($"* {attr.Name} = '{this.GetType().GetField(attr.Name).GetValue(this)}'\n");
94 | return str;
95 | }
96 |
97 | ///
98 | /// Constructs a new Sfo object, using the SFO data passed as parameter.
99 | ///
100 | /// The SFO data.
101 | public Sfo(byte[] sfo)
102 | {
103 | if (!MemCmp(sfo, sfo_magic, 0))
104 | {
105 | Console.Error.WriteLine("[ERROR] Not an SFO file");
106 | return;
107 | }
108 | if (GetLe32(sfo, 4) != 0x00000101)
109 | {
110 | Console.Error.WriteLine("[ERROR] Only SFO version 1.1 is supported");
111 | return;
112 | }
113 | Category = GetValue(sfo, "CATEGORY");
114 | // Convert App and Sys versions to (major * 100 + minor)
115 | AppVer = GetVersionFromString(GetValue(sfo, "APP_VER"));
116 | if (AppVer == 0)
117 | AppVer = null;
118 | SysVer = GetVersionFromBcd(GetValue(sfo, "PSP2_SYSTEM_VER"));
119 | if (SysVer == 0)
120 | SysVer = null;
121 | string pubtoolinfo = GetValue(sfo, "PUBTOOLINFO");
122 | if ((pubtoolinfo != null) && pubtoolinfo.Contains("c_date="))
123 | {
124 | var date = pubtoolinfo.Substring(pubtoolinfo.IndexOf("c_date=") + "c_date=".Length, 8);
125 | CDate = UInt32.Parse(date);
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/.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 | *.jfm
193 | *.pfx
194 | *.publishsettings
195 | node_modules/
196 | orleans.codegen.cs
197 |
198 | # Since there are multiple workflows, uncomment next line to ignore bower_components
199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
200 | #bower_components/
201 |
202 | # RIA/Silverlight projects
203 | Generated_Code/
204 |
205 | # Backup & report files from converting an old project file
206 | # to a newer Visual Studio version. Backup files are not needed,
207 | # because we have git ;-)
208 | _UpgradeReport_Files/
209 | Backup*/
210 | UpgradeLog*.XML
211 | UpgradeLog*.htm
212 |
213 | # SQL Server files
214 | *.mdf
215 | *.ldf
216 |
217 | # Business Intelligence projects
218 | *.rdl.data
219 | *.bim.layout
220 | *.bim_*.settings
221 |
222 | # Microsoft Fakes
223 | FakesAssemblies/
224 |
225 | # GhostDoc plugin setting file
226 | *.GhostDoc.xml
227 |
228 | # Node.js Tools for Visual Studio
229 | .ntvs_analysis.dat
230 |
231 | # Visual Studio 6 build log
232 | *.plg
233 |
234 | # Visual Studio 6 workspace options file
235 | *.opt
236 |
237 | # Visual Studio LightSwitch build output
238 | **/*.HTMLClient/GeneratedArtifacts
239 | **/*.DesktopClient/GeneratedArtifacts
240 | **/*.DesktopClient/ModelManifest.xml
241 | **/*.Server/GeneratedArtifacts
242 | **/*.Server/ModelManifest.xml
243 | _Pvt_Extensions
244 |
245 | # Paket dependency manager
246 | .paket/paket.exe
247 | paket-files/
248 |
249 | # FAKE - F# Make
250 | .fake/
251 |
252 | # JetBrains Rider
253 | .idea/
254 | *.sln.iml
255 |
256 | # CodeRush
257 | .cr/
258 |
259 | # Python Tools for Visual Studio (PTVS)
260 | __pycache__/
261 | *.pyc
262 |
263 | # Custom
264 | *.csv
265 | *.jpg
266 | *.jpeg
267 | *.html
268 | *.htm
269 | *.txt
270 | *.db
271 | *.xml
272 | *.zip
273 | *.7z
274 | *.rar
275 | *.org
276 | *.old
277 | sqlite3*
278 |
--------------------------------------------------------------------------------
/Settings.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using Microsoft.Extensions.Configuration;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.IO;
14 | using System.Linq;
15 | using System.Reflection;
16 |
17 | namespace VitaDB
18 | {
19 | public sealed class Settings
20 | {
21 | private static Settings instance;
22 | static private IConfigurationRoot config = null;
23 |
24 | public string application_name = Assembly.GetEntryAssembly().GetName().Name;
25 | public string database_name = Assembly.GetEntryAssembly().GetName().Name + ".db";
26 | public string local_sql = Assembly.GetEntryAssembly().GetName().Name + ".sql";
27 | public string remote_sql = "https://raw.githubusercontent.com/VitaSmith/VitaDB/master/VitaDB.sql";
28 | public string local_cache = "PkgCache.json";
29 | public string remote_cache = "https://raw.githubusercontent.com/VitaSmith/VitaDB/master/PkgCache.json";
30 | public static readonly string[] nps_type = new string[] { "App", "Demo", "DLC", "Theme", "PSM" };
31 | public static readonly int[] nps_category = { 1, 3, 101, 201, 601 };
32 | public string[] nps_url = new string[nps_type.Length];
33 | public int[] range = { 1, 1300 };
34 | public string csv_separator = " ";
35 | public bool csv_force_recheck = false;
36 |
37 | public List regions = null;
38 | public List regions_to_check = null;
39 | public Dictionary languages = new Dictionary();
40 | public Dictionary csv_mapping = new Dictionary();
41 |
42 | public static Settings Instance
43 | {
44 | get
45 | {
46 | if (instance == null)
47 | instance = new Settings();
48 | return instance;
49 | }
50 | }
51 |
52 | private static void AssignFromConfigString(ref string member, string value)
53 | {
54 | try
55 | {
56 | if (config[value] != null)
57 | member = config[value];
58 | }
59 | catch (Exception) { }
60 | }
61 |
62 | private Settings()
63 | {
64 | try
65 | {
66 | config = new ConfigurationBuilder()
67 | .SetBasePath(Directory.GetCurrentDirectory())
68 | .AddIniFile(application_name + ".ini", false, true)
69 | .Build();
70 | }
71 | catch (System.IO.FileNotFoundException)
72 | {
73 | Console.WriteLine($"INI file '{application_name}.ini' was not found");
74 | return;
75 | }
76 | catch (Exception) { };
77 | if (config == null)
78 | return;
79 |
80 | AssignFromConfigString(ref database_name, "db:name");
81 | AssignFromConfigString(ref local_sql, "db:local_sql");
82 | AssignFromConfigString(ref remote_sql, "db:remote_sql");
83 | AssignFromConfigString(ref local_cache, "db:local_cache");
84 | AssignFromConfigString(ref remote_cache, "db:remote_cache");
85 | AssignFromConfigString(ref csv_separator, "csv:separator");
86 |
87 | for (int i = 0; i < nps_type.Length; i++)
88 | AssignFromConfigString(ref nps_url[i], "csv:nps_" + nps_type[i].ToLower());
89 |
90 | foreach (var property in typeof(App).GetProperties())
91 | csv_mapping.Add(property.Name, property.Name);
92 | foreach (var mapping in config.GetSection("csv_mapping").GetChildren())
93 | {
94 | if (!csv_mapping.Keys.Contains(mapping.Key))
95 | Console.Error.WriteLine($"[WARNING] Ignoring csv_mapping '{mapping.Key}' as it is not a valid field name");
96 | else if (!String.IsNullOrEmpty(mapping.Value))
97 | csv_mapping[mapping.Key] = mapping.Value;
98 | }
99 |
100 | foreach (var region in config.GetSection("regions").GetChildren())
101 | {
102 | if (regions == null)
103 | regions = new List();
104 | regions.Add(region.Key);
105 | }
106 | // Set default region list if none were provided
107 | if (regions == null)
108 | regions = new List { "PCSA", "PCSB", "PCSC", "PCSD", "PCSE", "PCSF", "PSCG", "PCSH" };
109 |
110 | foreach (var region in config.GetSection("languages").GetChildren())
111 | {
112 | languages.Add(region.Key, region.Value);
113 | }
114 |
115 | try
116 | {
117 | var tmp_range = new int[2];
118 | var range_list = config["limits:range"].Split(',').ToArray();
119 | if (range_list.Length == 2)
120 | {
121 | tmp_range[0] = int.Parse(range_list[0]);
122 | tmp_range[1] = int.Parse(range_list[1]);
123 | if ((tmp_range[0] > 0) && (tmp_range[1] > 0) && (tmp_range[0] <= tmp_range[1]))
124 | range = tmp_range;
125 | else
126 | Console.WriteLine("Could not parse title_id range");
127 | }
128 | }
129 | catch (Exception) { };
130 |
131 | string r = "ALL";
132 | try
133 | {
134 | r = config["limits:region"];
135 | if (!regions.Contains(r))
136 | r = "ALL";
137 | }
138 | catch (Exception) { };
139 | regions_to_check = (r == "ALL") ? regions : new List { r };
140 | }
141 |
142 | ///
143 | /// Convert a CONTENT_ID to a region.
144 | ///
145 | /// The content ID.
146 | /// A 3 letter region name.
147 | public string GetRegionName(string content_id)
148 | {
149 | if (content_id.Substring(20, 4) == "ASIA")
150 | return "ASN";
151 | try
152 | {
153 | return config["regions:" + content_id.Substring(7, 4)];
154 | }
155 | catch (Exception)
156 | {
157 | return "???";
158 | };
159 | }
160 |
161 | ///
162 | /// Return the language code associated with a region.
163 | ///
164 | /// A string starting with a Vita region code (e.g. "PCSG").
165 | /// If the string is longer than 4 characters, only the first are used as the region code.
166 | /// A lowercase language code string (e.g. "ja-jp").
167 | public string GetLanguage(string region)
168 | {
169 | if (region == null)
170 | return "en-us";
171 | languages.TryGetValue((region.Length > 4) ? region.Substring(0, 4) : region, out string value);
172 | return value ?? "en-us";
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/Database.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using Microsoft.EntityFrameworkCore;
11 | using Microsoft.EntityFrameworkCore.Infrastructure;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using Microsoft.Extensions.Logging;
14 | using System;
15 | using System.Collections.Generic;
16 | using System.IO;
17 | using System.Net;
18 | using System.Reflection;
19 |
20 | /* DB Schema:
21 |
22 | CREATE TABLE IF NOT EXISTS `Apps` (
23 | `TITLE_ID` TEXT NOT NULL,
24 | `NAME` TEXT,
25 | `ALT_NAME` TEXT,
26 | `CONTENT_ID` TEXT NOT NULL UNIQUE,
27 | `PARENT_ID` TEXT,
28 | `CATEGORY` INTEGER,
29 | `PKG_ID` INTEGER UNIQUE,
30 | `ZRIF` TEXT,
31 | `COMMENTS` TEXT,
32 | `FLAGS` INTEGER NOT NULL DEFAULT 0,
33 | PRIMARY KEY(`CONTENT_ID`),
34 | FOREIGN KEY(`CATEGORY`) REFERENCES `Categories`(`VALUE`)
35 | FOREIGN KEY(`PKG_ID`) REFERENCES `Pkgs`(`ID`),
36 | );
37 |
38 | CREATE TABLE IF NOT EXISTS `Pkgs` (
39 | `ID` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
40 | `URL` TEXT UNIQUE,
41 | `SIZE` INTEGER,
42 | `SHA1` TEXT UNIQUE,
43 | `CATEGORY` TEXT,
44 | `APP_VER` INTEGER,
45 | `SYS_VER` INTEGER,
46 | `C_DATE` INTEGER,
47 | `V_DATE` INTEGER,
48 | `COMMENTS` TEXT
49 | );
50 |
51 | CREATE TABLE IF NOT EXISTS `Updates` (
52 | `CONTENT_ID` TEXT NOT NULL,
53 | `VERSION` INTEGER NOT NULL,
54 | `TYPE` INTEGER NOT NULL,
55 | `PKG_ID` INTEGER NOT NULL UNIQUE,
56 | PRIMARY KEY(`PKG_ID`),
57 | FOREIGN KEY(`CONTENT_ID`) REFERENCES `Apps`(`CONTENT_ID`),
58 | FOREIGN KEY(`TYPE`) REFERENCES `Types`(`VALUE`)
59 | FOREIGN KEY(`PKG_ID`) REFERENCES `Pkgs`(`ID`),
60 | );
61 |
62 | CREATE TABLE IF NOT EXISTS `Categories` (
63 | `NAME` TEXT NOT NULL UNIQUE,
64 | `VALUE` INTEGER NOT NULL UNIQUE,
65 | PRIMARY KEY(`VALUE`)
66 | );
67 |
68 | CREATE TABLE IF NOT EXISTS `Flags` (
69 | `NAME` TEXT NOT NULL UNIQUE,
70 | `VALUE` INTEGER NOT NULL UNIQUE,
71 | PRIMARY KEY(`VALUE`)
72 | );
73 |
74 | CREATE TABLE IF NOT EXISTS `Types` (
75 | `NAME` TEXT NOT NULL UNIQUE,
76 | `VALUE` INTEGER NOT NULL UNIQUE,
77 | PRIMARY KEY(`VALUE`)
78 | );
79 |
80 | */
81 |
82 | namespace VitaDB
83 | {
84 | // Optional logging to file
85 | public class SQLLoggerProvider : ILoggerProvider
86 | {
87 | public ILogger CreateLogger(string cat_name)
88 | {
89 | File.Delete(Assembly.GetEntryAssembly().GetName().Name + ".log");
90 | return new SQLLogger();
91 | }
92 |
93 | public void Dispose()
94 | { }
95 |
96 | private class SQLLogger : ILogger
97 | {
98 | public bool IsEnabled(LogLevel log_level)
99 | {
100 | return true;
101 | }
102 |
103 | public void Log(LogLevel log_level, EventId event_id, TState state, Exception exception, Func formatter)
104 | {
105 | File.AppendAllText(Assembly.GetEntryAssembly().GetName().Name + ".log", formatter(state, exception));
106 | //Debug.WriteLine(formatter(state, exception));
107 | }
108 |
109 | public IDisposable BeginScope(TState state)
110 | {
111 | return null;
112 | }
113 | }
114 | }
115 |
116 | public class Database : DbContext
117 | {
118 | public DbSet Apps { get; set; }
119 | private DbSet Categories { get; set; }
120 | private DbSet Flags { get; set; }
121 | public DbSet Pkgs { get; set; }
122 | public DbSet Updates { get; set; }
123 | private DbSet Types { get; set; }
124 | // Shorthands for converting Categories and Flags into a value
125 | public readonly Dictionary Category;
126 | public readonly Dictionary Flag;
127 | public readonly Dictionary Type;
128 |
129 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
130 | {
131 | optionsBuilder.UseSqlite($"Data Source={Settings.Instance.database_name}");
132 | optionsBuilder.EnableSensitiveDataLogging();
133 | }
134 |
135 | ///
136 | /// Create a new database context.
137 | ///
138 | /// (Optional) Log Entity Framework data.
139 | public Database(bool initial_setup = false, bool logging = false) : base()
140 | {
141 | if (!initial_setup)
142 | {
143 | Category = new Dictionary();
144 | foreach (var category in Categories)
145 | Category[category.NAME] = category.VALUE;
146 | Flag = new Dictionary();
147 | foreach (var flag in Flags)
148 | Flag[flag.NAME] = (UInt16)flag.VALUE;
149 | Type = new Dictionary();
150 | foreach (var type in Types)
151 | Type[type.NAME] = type.VALUE;
152 | }
153 | if (logging)
154 | {
155 | var sp = this.GetInfrastructure();
156 | var lf = sp.GetService();
157 | lf.AddProvider(new SQLLoggerProvider());
158 | }
159 | }
160 |
161 | ///
162 | /// (Re)create the SQLite Database from an SQL dump.
163 | ///
164 | /// (Optional) If true, this will recreate the database even if it exists.
165 | /// True on success, false on error.
166 | public static bool CreateDB(string sql_file, bool recreate = false)
167 | {
168 | if (recreate)
169 | {
170 | try
171 | {
172 | File.Delete(Settings.Instance.database_name);
173 | }
174 | catch (System.IO.IOException e)
175 | {
176 | Console.Error.WriteLine("[ERROR] " + e.Message);
177 | return false;
178 | }
179 | }
180 | Console.WriteLine($"Creating database '{Settings.Instance.database_name}' from '{sql_file}'...");
181 | var lines = File.ReadAllLines(sql_file);
182 | using (var db = new Database(true))
183 | using (var connection = db.Database.GetDbConnection())
184 | {
185 | connection.Open();
186 | {
187 | using (var command = connection.CreateCommand())
188 | {
189 | foreach (var line in lines)
190 | {
191 | command.CommandText += line;
192 | if (line.EndsWith(';'))
193 | {
194 | command.ExecuteNonQuery();
195 | command.CommandText = "";
196 | }
197 | }
198 | // Turn FOREIGN KEYS on, just in case...
199 | command.CommandText = "PRAGMA foreign_keys = ON;";
200 | command.ExecuteNonQuery();
201 | }
202 | }
203 | connection.Close();
204 | }
205 | return true;
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Rif.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using ICSharpCode.SharpZipLib.Zip.Compression;
11 | using System;
12 | using System.Linq;
13 |
14 | using static VitaDB.Utilities;
15 |
16 | namespace VitaDB
17 | {
18 | class RIF
19 | {
20 | public static readonly UInt32 ZLIB_DICTIONARY_ID_ZRIF = 0x627d1d5d;
21 | public static readonly byte[] zrif_dict = {
22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
24 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
25 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
26 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
27 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
29 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
30 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
31 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
32 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
33 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
34 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
43 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48,
44 | 48, 48, 54, 48, 48, 48, 48, 55, 48, 48, 48, 48, 56, 0, 48, 48, 48, 48, 51, 48, 48, 48, 48, 52, 48, 48, 48, 48,
45 | 53, 48, 95, 48, 48, 45, 65, 68, 68, 67, 79, 78, 84, 48, 48, 48, 48, 50, 45, 80, 67, 83, 71, 48, 48, 48, 48,
46 | 48, 48, 48, 48, 48, 48, 49, 45, 80, 67, 83, 69, 48, 48, 48, 45, 80, 67, 83, 70, 48, 48, 48, 45, 80, 67, 83,
47 | 67, 48, 48, 48, 45, 80, 67, 83, 68, 48, 48, 48, 45, 80, 67, 83, 65, 48, 48, 48, 45, 80, 67, 83, 66, 48, 48,
48 | 48, 0, 1, 0, 1, 0, 1, 0, 2, 239, 205, 171, 137, 103, 69, 35, 1
49 | };
50 | public static readonly byte[] rif_name_key = {
51 | 0x19, 0xDD, 0x4F, 0xB9, 0x89, 0x48, 0x2B, 0xD4,
52 | 0xCB, 0x9E, 0xC9, 0xC7, 0x9A, 0x2E, 0xFB, 0xD0
53 | };
54 |
55 | // https://gist.github.com/TheOfficialFloW/30c90b95c35623dda8a367996b4c08b8
56 | ///
57 | /// Get the RIF name associated with a specific AID.
58 | ///
59 | /// The mode to use (0 = bounded, 1 = fixed).
60 | /// The user's 64-bit PSN Account ID.
61 | /// The RIF name.
62 | public static string GetRifName(UInt64 mode, UInt64 aid)
63 | {
64 | if (!BitConverter.IsLittleEndian)
65 | {
66 | // Values must be copied in little endian mode
67 | aid = SwapBytes(aid);
68 | mode = SwapBytes(mode);
69 | }
70 | byte[] data = new byte[0x10];
71 | Buffer.BlockCopy(BitConverter.GetBytes(mode), 0, data, 0, 8);
72 | Buffer.BlockCopy(BitConverter.GetBytes(aid), 0, data, 8, 8);
73 |
74 | return BitConverter.ToString(AesEncrypt(data, rif_name_key), 0, 0x10)
75 | .Replace("-", "").ToLower() + ".rif";
76 | }
77 |
78 | ///
79 | /// Get the AID from a RIF name.
80 | ///
81 | /// The RIF name.
82 | /// The 64-bit AID value (as per ux0's 'id.dat')
83 | public static UInt64 GetAidFromRifName(string rif_name)
84 | {
85 | byte[] data = AesDecrypt(HexStringToByteArray(rif_name.Split('.').First()), rif_name_key);
86 | if (!MemCmp(data, new byte[7], 1))
87 | throw new ApplicationException($"Decoded RIF mode 0x'{GetLe64(data):X8}' does not match 0 or 1");
88 | return GetLe64(data, 8);
89 | }
90 |
91 | ///
92 | /// Decode a zRIF encoded string to a RIF byte array.
93 | ///
94 | /// The zRIF string
95 | /// The RIF byte array, or null on error
96 | public static byte[] Decode(string zrif)
97 | {
98 | byte[] input, output = new byte[1024];
99 |
100 | try
101 | {
102 | // Pad string if not a multiple of 4
103 | if (zrif.Length % 4 != 0)
104 | zrif += new string('=', 4 - (zrif.Length % 4));
105 | input = Convert.FromBase64String(zrif);
106 | }
107 | catch (Exception e) when (e is System.FormatException || e is System.NullReferenceException)
108 | {
109 | Console.Error.WriteLine($"[ERROR] {e.Message}");
110 | return null;
111 | }
112 |
113 | if (input.Length < 6)
114 | {
115 | Console.Error.WriteLine("[ERROR] zRIF length too short");
116 | return null;
117 | }
118 | if (((input[0] << 8) + input[1]) % 31 != 0)
119 | {
120 | Console.Error.WriteLine("[ERROR] zRIF header is corrupted");
121 | return null;
122 | }
123 | var inflater = new Inflater();
124 | inflater.SetInput(input);
125 | inflater.Inflate(output);
126 | if (inflater.IsNeedingDictionary)
127 | inflater.SetDictionary(zrif_dict);
128 | switch(inflater.Inflate(output))
129 | {
130 | case 1024:
131 | return output;
132 | case 512:
133 | return MemCpy(output, 0, 512);
134 | default:
135 | return null;
136 | }
137 | }
138 |
139 | ///
140 | /// Encode a RIF byte array to zRIF string.
141 | ///
142 | /// The zRIF string.
143 | /// The zRIF string or null on error.
144 | public static string Encode(byte[] rif)
145 | {
146 | byte[] output = new byte[128 + 2];
147 |
148 | if ((rif.Length != 512) && (rif.Length != 1024))
149 | {
150 | Console.Error.WriteLine("[ERROR] invalid RIF length");
151 | return null;
152 | }
153 | var deflater = new Deflater();
154 | deflater.SetDictionary(zrif_dict);
155 | deflater.SetInput(rif);
156 | deflater.SetLevel(Deflater.BEST_COMPRESSION);
157 | deflater.SetStrategy(DeflateStrategy.Default);
158 | deflater.Finish();
159 | int size = deflater.Deflate(output, 0, output.Length - 2);
160 | if (!deflater.IsFinished)
161 | {
162 | Console.Error.WriteLine("[ERROR] Deflate error");
163 | return null;
164 | }
165 | // Don't have that much control over Window size so our header needs to be adjusted.
166 | if ((output[0] == 0x78) && (output[1] == 0xF9))
167 | {
168 | output[0] = 0x28;
169 | output[1] = 0xEE;
170 | }
171 | // Adjust the size to be a multiple of 3 so that we don't get padding
172 | return Convert.ToBase64String(output, 0, ((size + 2) / 3) * 3);
173 | }
174 |
175 | ///
176 | /// Retrieve the CONTENT_ID field from a zRIF encoded key.
177 | ///
178 | /// The 512-byte RIF byte array.
179 | /// The CONTENT_ID string or null on error.
180 | public static string GetContentId(string zrif)
181 | {
182 | if (zrif == null)
183 | return null;
184 | var rif = RIF.Decode(zrif);
185 | if (rif == null)
186 | return null;
187 | // PSM and regular RIFs have their CONTENT_ID at different offsets
188 | var content_id = MemCpy(rif, MemCmp(rif, new byte[8]) ? 0x50 : 0x10, 0x24);
189 | try
190 | {
191 | return System.Text.ASCIIEncoding.Default.GetString(content_id);
192 | }
193 | catch (Exception)
194 | {
195 | return null;
196 | }
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | VitaDB - A Database for PS Vita content
2 | =======================================
3 |
4 | Description
5 | -----------
6 |
7 | This is a database of Vita content, that includes pretty much of all the games, applications, DLCs, updates, etc. that
8 | are currently publicly known, and that is provided in the easy-to-work-with and universally supported SQLite format, as
9 | well as a Windows/Linux/MacOS .NET Core 2.0 application that demonstrates how the database can be manipulated.
10 |
11 | ### Target Audience
12 |
13 | This project is intended for developers or web site creators, who may be interested in building applications that
14 | revolve around the provision of Vita content. Like a library (`.dll`, `.so`) it is not meant to be used directly by
15 | end-users.
16 |
17 | ### Purpose
18 |
19 | The availability of this database can, we hope, help with many new possibilities.
20 |
21 | One would be the recreation of one's legally owned content in a manner that is both a lot more convenient and much much
22 | faster than what can be achieved through Sony's very limited content management application as well as its annoyingly
23 | non-user friendly and __sloooooow__ proprietary memory card format.
24 |
25 | Another would be with the cataloguing and updating of public Vita content, using PSDLE data, through a web service.
26 |
27 | Yet another possibility would be cataloguing applications that can either run on the Vita itself or from a PC with
28 | access to the Vita memory card.
29 |
30 | All in all, we hope that this database can become the de-facto, up-to-date repository for any application that needs to
31 | process Vita content.
32 |
33 | ### About NoNpDrm and zRIF content
34 |
35 | You will __NOT__ find any zRIFs (licenses) in the data provided in this project because its goal is to keep track of
36 | official PS Vita content only.
37 |
38 | Still, for people who might want to back up their own content licenses using NoNpDrm, the database does provides a
39 | placeholder for zRIFs, and the application also demonstrates how zRIF data can be imported. But that's about it.
40 |
41 | Usage
42 | -----
43 |
44 | You must have sorted out to run .NET Core 2.0 applications on your machine. If you don't know how, please google it.
45 |
46 | ```
47 | Usage: VitaDB [OPTIONS]
48 |
49 | Options:
50 | -m, --maintenance perform database maintenance
51 | -i, --input=VALUE name of the input file or URL
52 | -o, --output=VALUE name of the output file
53 | -c, --csv import/export CSV (Content type is deduced from filename)
54 | -n, --nps import data from NoPayStation online spreadsheet
55 | --chihiro refresh db from Chihiro
56 | --psn refresh db from PSN
57 | --region internal region check
58 | -d, --dump dump database to SQL (requires sqlite3.exe)
59 | -p, --purge purge/create a new PKG cache dictionary
60 | -u, --url=VALUE update DB from PSN Store/Pkg URL(s)
61 | --version display version and exit
62 | -v increase verbosity
63 | -z, --zrif import/export zRIFs
64 | -w, --wait-for-key wait for keypress before exiting
65 | -h, --help show this message and exit
66 | ```
67 |
68 | Examples:
69 |
70 | ```
71 | dotnet VitaDB.dll -n
72 | dotnet VitaDB.dll -c -o apps.csv
73 | dotnet VitaDB.dll -c -o dlc.csv
74 | dotnet VitaDB.dll -u url_list.txt
75 | dotnet VitaDB.dll --psn
76 | dotnet VitaDB.dll -m
77 | ```
78 |
79 | Once the database has been created/updated, you can use an SQLite local browser, such as the multiplatform one from http://sqlitebrowser.org/,
80 | to view the data.
81 |
82 | You can also edit the content of the `.ini` file according to your requirements.
83 |
84 | Database structure
85 | ------------------
86 |
87 | ### Apps
88 |
89 | * `TITLE_ID` The 9 alphanumeric character identifier of a title.
90 | Note that the first 4 characters also identify the region (you can see the region table in the `ini` file).
91 | Because `TITLE_ID` is always contained in `CONTENT_ID`, this column is redundant, but provided for convenience.
92 | * `NAME` The __official__ name of the game, as per the Sony servers (including any typos and ® or ™, but trimmed
93 | of leading or trailing white spaces and LFs)
94 | * `ALT_NAME` A user defined alternate name. This can be used, for instance, for the English translation of a
95 | Japanese title.
96 | * `CONTENT_ID` __THE__ identifier of a published App/Game/Addon/Theme/Avatar. This is what Sony uses to __uniquely__
97 | identify content, so we do too. As such, this is the table's _primary key_. As mentioned earlier, `CONTENT_ID`
98 | always contains the `TITLE_ID`.
99 | * `PARENT_ID` The `CONTENT_ID` of any parent application. For instance, `PARENT_ID` can be used with an App/Game to
100 | link to the `CONTENT_ID` of a bundle (e.g a PS3 or PS4 multiplatform purchase) or, for DLCs, to the `CONTENT_ID`
101 | of the App the DLC applies to.
102 | * `CATEGORY` An integer representing the application category. See the __Categories__ table below.
103 | * `PKG_ID` The optional `ID` of a PKG entry from the Pkgs table. This is basically used to access the official URL
104 | where one can download content from the Sony servers.
105 | * `ZRIF` A __PLACEHOLDER__ field for zRIFs. This is provided as a placeholder so that applications that use this DB
106 | can use this field to store your __legally owned__ licenses, in your personal copy of the DB.
107 | * `COMMENTS` A placeholder for comments.
108 | * `FLAGS` A set of binary flags, that are used to set specific fields to read-only, so that they don't get altered
109 | when importing data, or to indicate if an application is a free. See the __Flags__ table below for details.
110 |
111 | ### Pkgs
112 |
113 | * `ID` An auto incremented integer ID to uniquely identify each pkg link. This is done so that we don't have to
114 | use the very long URLs as primary keys.
115 | * `URL` A unique PKG URl. The URLs can indiscriminately be for games, apps, DLC, themes, patches, etc.
116 | * `SIZE` The size of the PKG as read from the servers. You should use a 64-bit integer to map this, as it can be more
117 | than 4 GB.
118 | * `SHA1` The hex representation of the Pkg SHA1 (last 20 bytes from end-0x20). For convenience, this is being stored
119 | as a string (that needs to be converted) rather than a byte array.
120 | * `CATEGORY` A short sequence of letters describing the pkg category. This data mostly comes from the SFO.
121 | * `APP_VER` The application version, represented as the decimal number major *100 + minor (e.g. `100` for `v1.0`)
122 | * `SYS_VER` The minimum required system version, represented as the decimal number major *100 + minor (e.g. `355` for `v3.55`)
123 | * `C_DATE` The date when the application was created, in `YYYYMMDD` format. This data comes from the SFO.
124 | * `V_DATE` The date when the PKG was last retrieved, in `YYYYMMDD` format.
125 | * `COMMENTS` A placeholder for comments.
126 |
127 | ### Updates
128 |
129 | * `CONTENT_ID` The application this update applies to.
130 | * `VERSION` The application version, represented as the decimal number major *100 + minor (e.g. `210` for `v2.10`)
131 | * `TYPE` The type of update. See the __Types__ table below.
132 | * `PKG_ID` The `ID` of a update PKG entry in the Pkgs table. This is the primary key.
133 |
134 | ## Categories
135 |
136 | This table contains the values used for `CATEGORY` used by the __Apps__ table.
137 |
138 | The values are organized so that Games/Apps should have a value that is < 100, DLC < 200, Themes < 300, etc.
139 | This is done so that searching for Games/Apps, DLC and so on, in the Apps table, can be carried out very easily.
140 |
141 | ## Flags
142 |
143 | This table contains the values used for `FLAGS` in the __Apps__ table. Each value is a power of 2 that, if
144 | found in the `FLAGS` column, indicates whether a specific Flag is active or not. The `_RO` values are for
145 | flags indicating whether a column should be read-only, whereas `FREE_APP` indicates applications that don't
146 | need a license to run.
147 |
148 | ### Types
149 |
150 | This is used by the __Updates__ table to denote the type of update. There are currently only 3 types: `cumulative`,
151 | `incremental` and `hybrid`, which are pretty much a direct mapping of the types reported by the Sony update XML data.
152 |
153 | FAQ
154 | ---
155 |
156 | ### Why store the SHA-1?
157 |
158 | Besides being useful to check for pkg corruption, this can also be done to detect any silent changes that Sony may
159 | apply to PKG data. For instance, it is not difficult to imagine that, should Sony decide they've had enough of
160 | Henkaku/Enso 3.60 users who download pkg data off their servers, they could relatively easily modify all or a choice
161 | set of pkg they serve with an `eboot` to force a silent system upgrade to 3.61+.
162 | By storing a copy of the PKG SHA-1, along with the date when that SHA-1 was retrieved, it becomes possible to detect
163 | this kind of "silent upgrade" scenario.
164 |
165 | ### Why is `CONTENT_ID` used as primary key, rather than `TITLE_ID`?
166 |
167 | That's because you can have both a DEMO and FULL game served from same TITLE_ID, with
168 | different PKG urls and CONTENT_ID. For instance:
169 | * 漢字 点つなぎセット (DEMO) = `JP9000-PCSC00038_00-PP2JP00000000001`
170 | * 漢字 点つなぎセット (FULL) = `JP9000-PCSC00038_00-KANJITEN00000001`
171 |
172 | ### What's the purpose of `PkgCache.json`?
173 |
174 | `PkgCache.json` is a locally cached version of a PKG URL to `CONTENT_ID` dictionary.
175 | This avoids having to perform a time consuming round trip to the Sony PKG servers every time we want to validate the
176 | `CONTENT_ID` of a PKG, which is a very frequent operation. The JSON data is simply the serialized version of internal
177 | dictionary we use.
178 |
179 | One may consider that this duplicates data that is already present in the database, but it's really the other way
180 | around: this is __external__ data that is needed to validate the consistency of the database, and that we duplicate
181 | locally to avoid having to query the PKG servers.
182 |
183 | Note that you can use the `-p` option if you want to recreate your own `PkgCache.json` instead of reusing the one from
184 | the gtihub project.
185 |
186 | ### Why isn't the SHA-1 column set to be UNIQUE?
187 |
188 | Because Sony has found nothing better than provide the exact same content (a 10 GB... patch?!?) for Phantasy Star Online
189 | under 2 different URLs:
190 | * `http://gs.ww.np.dl.playstation.net/ppkg/np/PCSG00141/PCSG00141_T175/7e215919b18a27eb/JP0177-PCSG00141_00-PHANTASYSTARONL2-A0500-V0100-b501028c7b01305693f0b5768dadf2ef71c553b6-PE.pkg`
191 | * `http://gs.ww.np.dl.playstation.net/ppkg/np/PCSG00141/PCSG00141_T176/4f153d40849e6856/JP0177-PCSG00141_00-PHANTASYSTARONL2-A0500-V0100-313ed2cf66018279e89ba59c44db9a7de1e4b287-PE.pkg`
192 |
--------------------------------------------------------------------------------
/Update.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using Microsoft.EntityFrameworkCore;
11 | using System;
12 | using System.ComponentModel.DataAnnotations;
13 | using System.ComponentModel.DataAnnotations.Schema;
14 | using System.Linq;
15 | using System.Net;
16 | using System.Security.Cryptography;
17 | using System.Text;
18 | using System.Xml;
19 | using System.Xml.Linq;
20 | using System.Xml.Serialization;
21 |
22 | using static VitaDB.Utilities;
23 |
24 | namespace VitaDB
25 | {
26 | // XML Elements for PCS######-ver.xml deserialization
27 | [Serializable, XmlType("titlepatch")]
28 | public class TitlePatch
29 | {
30 | [XmlAttribute(AttributeName = "status")]
31 | public string Status { get; set; }
32 |
33 | [XmlAttribute(AttributeName = "titleid")]
34 | public string TitleId { get; set; }
35 |
36 | [XmlElement(typeof(Tag), ElementName = "tag")]
37 | public Tag Tag { get; set; }
38 |
39 | [XmlAnyAttribute]
40 | public XmlAttribute[] AnyAttributes;
41 |
42 | [XmlAnyElement]
43 | public XmlElement[] AnyElements;
44 | }
45 |
46 | public class Tag
47 | {
48 | [XmlAttribute(AttributeName = "name")]
49 | public string Name { get; set; }
50 |
51 | [XmlAttribute(AttributeName = "signoff")]
52 | public bool Signoff { get; set; }
53 |
54 | [XmlElement(typeof(Package), ElementName = "package")]
55 | public Package[] Packages { get; set; }
56 |
57 | [XmlAnyAttribute]
58 | public XmlAttribute[] AnyAttributes;
59 |
60 | [XmlAnyElement]
61 | public XmlElement[] AnyElements;
62 | }
63 |
64 | public class Package
65 | {
66 | [XmlAttribute(AttributeName = "version")]
67 | public string Version { get; set; }
68 |
69 | [XmlAttribute(AttributeName = "type")]
70 | public string Type { get; set; }
71 |
72 | [XmlAttribute(AttributeName = "size")]
73 | public UInt64 Size { get; set; }
74 |
75 | [XmlAttribute(AttributeName = "sha1sum")]
76 | public string Sha1Sum { get; set; }
77 |
78 | [XmlAttribute(AttributeName = "url")]
79 | public string Url { get; set; }
80 |
81 | [XmlAttribute(AttributeName = "psp2_system_ver")]
82 | public UInt32 SysVer { get; set; }
83 |
84 | [XmlAttribute(AttributeName = "content_id")]
85 | public string ContentId { get; set; }
86 |
87 | [XmlElement(ElementName = "paramsfo")]
88 | public ParamSfo Sfo { get; set; }
89 |
90 | [XmlElement(ElementName = "changeinfo")]
91 | public ChangeInfo Info { get; set; }
92 |
93 | [XmlElement(typeof(Package), ElementName = "hybrid_package")]
94 | public Package HybridPackage { get; set; }
95 |
96 | [XmlAnyAttribute]
97 | public XmlAttribute[] AnyAttributes;
98 |
99 | [XmlAnyElement]
100 | public XmlElement[] AnyElements;
101 | }
102 |
103 | public class ParamSfo
104 | {
105 | [XmlElement(ElementName = "title")]
106 | public string Title { get; set; }
107 |
108 | [XmlAnyAttribute]
109 | public XmlAttribute[] AnyAttributes;
110 |
111 | [XmlAnyElement]
112 | public XmlElement[] AnyElements;
113 | }
114 |
115 | public class ChangeInfo
116 | {
117 | [XmlAttribute(AttributeName = "url")]
118 | public string Url { get; set; }
119 |
120 | [XmlAnyAttribute]
121 | public XmlAttribute[] AnyAttributes;
122 |
123 | [XmlAnyElement]
124 | public XmlElement[] AnyElements;
125 | }
126 |
127 | public class Update
128 | {
129 | public string CONTENT_ID { get; set; }
130 | public UInt32 VERSION { get; set; }
131 | public int TYPE { get; set; }
132 | [Key]
133 | public UInt32 PKG_ID { get; set; }
134 |
135 | [NotMapped]
136 | public string URL { get; set; }
137 | [NotMapped]
138 | public UInt64 Size;
139 | [NotMapped]
140 | public byte[] Sha1Sum;
141 | [NotMapped]
142 | public UInt32 SysVer;
143 |
144 | // The SHA-256 key used to convert a TITLE_ID to an update hash
145 | private static readonly byte[] hmac_sha256_key =
146 | {
147 | 0xE5, 0xE2, 0x78, 0xAA, 0x1E, 0xE3, 0x40, 0x82, 0xA0, 0x88, 0x27, 0x9C, 0x83, 0xF9, 0xBB, 0xC8,
148 | 0x06, 0x82, 0x1C, 0x52, 0xF2, 0xAB, 0x5D, 0x2B, 0x4A, 0xBD, 0x99, 0x54, 0x50, 0x35, 0x51, 0x14
149 | };
150 | private static HMACSHA256 hmac = new HMACSHA256(hmac_sha256_key);
151 | private static readonly string base_url = "https://gs-sec.ww.np.dl.playstation.net/pl/np/";
152 |
153 | ///
154 | /// Upsert an entry into the DB.
155 | ///
156 | /// The database context.
157 | /// (Optional) Detach the entry after upsert.
158 | public void Upsert(Database db, bool detach = false)
159 | {
160 | if (db.Updates.Any(x => x.PKG_ID == PKG_ID))
161 | db.Entry(this).State = EntityState.Modified;
162 | else
163 | db.Updates.Add(this);
164 | db.SaveChanges();
165 | if (detach)
166 | db.Entry(this).State = EntityState.Detached;
167 | }
168 |
169 | ///
170 | /// Fetch XML update data from the PSN update servers.
171 | ///
172 | /// The TITLE_ID to look for update data.
173 | /// An XML document with the update data, or null if no update is avaialble.
174 | private static XDocument GetUpdateData(string title_id)
175 | {
176 | byte[] hash = hmac.ComputeHash(new ASCIIEncoding().GetBytes("np_" + title_id));
177 | var url = base_url + title_id + "/" +
178 | BitConverter.ToString(hash).ToLower().Replace("-", "") + "/" + title_id + "-ver.xml";
179 |
180 | // Required to ignore Sony's self-signed certificate errors
181 | ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
182 | try
183 | {
184 | return XDocument.Load(url);
185 | }
186 | catch (System.Net.WebException)
187 | {
188 | // URL does not exist
189 | return null;
190 | }
191 | catch (System.Xml.XmlException)
192 | {
193 | // XML is empty
194 | return null;
195 | }
196 | }
197 |
198 | ///
199 | /// Check the updates PSN servers and insert/update the relevant DB records.
200 | ///
201 | /// The database context.
202 | /// The TITLE_ID to search updates for.
203 | public static void Check(Database db, string title_id)
204 | {
205 | var document = Update.GetUpdateData(title_id);
206 | if (document == null)
207 | return;
208 |
209 | TitlePatch patch = DeserializeFromXML(document);
210 | if (patch.Tag != null)
211 | {
212 | foreach (var update_pkg in Nullable(patch.Tag.Packages))
213 | {
214 | Update update = new Update
215 | {
216 | CONTENT_ID = update_pkg.ContentId,
217 | VERSION = GetVersionFromString(update_pkg.Version),
218 | TYPE = db.Type[update_pkg.Type ?? "cumulative"],
219 | URL = update_pkg.Url,
220 | Size = update_pkg.Size,
221 | Sha1Sum = HexStringToByteArray(update_pkg.Sha1Sum),
222 | SysVer = GetVersionFromBcd(update_pkg.SysVer),
223 | };
224 |
225 | var app = db.Apps.Where(x => x.CONTENT_ID == update.CONTENT_ID).FirstOrDefault();
226 | if (app == null)
227 | {
228 | app = new App
229 | {
230 | CONTENT_ID = update.CONTENT_ID,
231 | TITLE_ID = update.CONTENT_ID.Substring(7, 9),
232 | NAME = document.Descendants("title").FirstOrDefault().Value
233 | };
234 | Console.WriteLine($"[NOTE] Adding new App {app.CONTENT_ID}: {app.NAME}");
235 | db.Apps.Add(app);
236 | db.SaveChanges();
237 | }
238 |
239 | var pkg = db.Pkgs.Where(x => x.URL == update.URL).FirstOrDefault();
240 | if (pkg == null)
241 | {
242 | pkg = Pkg.CreatePkg(update.URL);
243 | if (pkg == null)
244 | continue;
245 | db.Pkgs.Add(pkg);
246 | db.SaveChanges();
247 | }
248 | update.PKG_ID = pkg.ID;
249 | update.Upsert(db, true);
250 |
251 | if (update_pkg.HybridPackage != null)
252 | {
253 | // Hybrid packages are derived from parent
254 | update.CONTENT_ID = update_pkg.HybridPackage.ContentId;
255 | update.TYPE = db.Type["hybrid"];
256 | update.URL = update_pkg.HybridPackage.Url;
257 | update.Size = update_pkg.HybridPackage.Size;
258 | update.Sha1Sum = HexStringToByteArray(update_pkg.HybridPackage.Sha1Sum);
259 |
260 | pkg = db.Pkgs.Where(x => x.URL == update.URL).FirstOrDefault();
261 | if (pkg == null)
262 | {
263 | pkg = Pkg.CreatePkg(update.URL);
264 | db.Pkgs.Add(pkg);
265 | db.SaveChanges();
266 | }
267 | update.PKG_ID = pkg.ID;
268 | update.Upsert(db, true);
269 | }
270 | }
271 | }
272 | }
273 | }
274 |
275 | // DB entries for Update.TYPE
276 | public class Type
277 | {
278 | public string NAME { get; set; }
279 | [Key]
280 | public int VALUE { get; set; }
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/App.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using Microsoft.EntityFrameworkCore;
11 | using System;
12 | using System.ComponentModel.DataAnnotations;
13 | using System.ComponentModel.DataAnnotations.Schema;
14 | using System.Linq;
15 | using System.Text.RegularExpressions;
16 |
17 | using static VitaDB.Utilities;
18 |
19 | namespace VitaDB
20 | {
21 | public class App
22 | {
23 | public string TITLE_ID { get; set; }
24 | public string NAME { get; set; }
25 | public string ALT_NAME { get; set; }
26 | [Key]
27 | public string CONTENT_ID { get; set; }
28 | public string PARENT_ID { get; set; }
29 | public int? CATEGORY { get; set; }
30 | public UInt32? PKG_ID { get; set; }
31 | public string ZRIF { get; set; }
32 | public string COMMENTS { get; set; }
33 | public UInt16 FLAGS { get; set; }
34 |
35 | [NotMapped]
36 | public string PKG_URL { get; set; }
37 |
38 | static private UInt32 count = 0;
39 |
40 | ///
41 | /// Set a flag or a set of flags.
42 | ///
43 | /// The database context.
44 | /// One or more names of flags to set.
45 | public void SetFlag(Database db, params string[] flag_names)
46 | {
47 | for (int i = 0; i < flag_names.Length; i++)
48 | this.FLAGS |= (UInt16)db.Flag[flag_names[i]];
49 | }
50 |
51 | ///
52 | /// Set an attribute to read-only.
53 | ///
54 | /// The database context.
55 | /// The names of one or more attrinbutes to set to read-only.
56 | public void SetReadOnly(Database db, params string[] attr_names)
57 | {
58 | for (int i = 0; i < attr_names.Length; i++)
59 | this.FLAGS |= (UInt16)db.Flag[attr_names[i] + "_RO"];
60 | }
61 |
62 | ///
63 | /// Add a new PARENT_ID entry.
64 | ///
65 | /// The ID to add.
66 | public void AddParent(string parent_id)
67 | {
68 | if (String.IsNullOrEmpty(parent_id))
69 | return;
70 | if (String.IsNullOrEmpty(this.PARENT_ID))
71 | this.PARENT_ID = parent_id;
72 | else if (!this.PARENT_ID.Contains(parent_id))
73 | this.PARENT_ID += " " + parent_id;
74 | }
75 |
76 | ///
77 | /// Insert or update a new application entry into the Apps database.
78 | /// This method preserves attributes that have been flagged to read-only.
79 | /// Note: This method only saves changes to the databse every 100 records.
80 | ///
81 | /// The database context.
82 | public void Upsert(Database db)
83 | {
84 | var app = db.Apps.Find(CONTENT_ID);
85 | if (app == null)
86 | {
87 | db.Apps.Add(this);
88 | }
89 | else
90 | {
91 | var org_app = db.Apps
92 | .AsNoTracking()
93 | .FirstOrDefault(x => x.CONTENT_ID == this.CONTENT_ID);
94 | if (org_app == null)
95 | {
96 | // Changes need to be applied
97 | db.SaveChanges();
98 | org_app = db.Apps
99 | .AsNoTracking()
100 | .FirstOrDefault(x => x.CONTENT_ID == this.CONTENT_ID);
101 | if (org_app == null)
102 | throw new ApplicationException("Tracked App found, but database changes were not applied.");
103 | }
104 | var entry = db.Entry(this);
105 |
106 | foreach (var attr in typeof(App).GetProperties())
107 | {
108 | if ((attr.Name == nameof(PKG_URL)) || (attr.Name == nameof(CONTENT_ID)))
109 | continue;
110 |
111 | // Manually Check for values that have been modified
112 | var new_value = attr.GetValue(this, null);
113 | var org_value = attr.GetValue(org_app, null);
114 | if ((new_value == null) || new_value.Equals(org_value))
115 | {
116 | entry.Property(attr.Name).IsModified = false;
117 | continue;
118 | }
119 |
120 | // Set modified attribute according to the read-only flags
121 | if (db.Flag.ContainsKey(attr.Name + "_RO"))
122 | {
123 | entry.Property(attr.Name).IsModified =
124 | ((org_app.FLAGS & db.Flag[attr.Name + "_RO"]) == 0);
125 | }
126 | else
127 | {
128 | entry.Property(attr.Name).IsModified = true;
129 | }
130 | }
131 |
132 | // Flags can only be added in this method, never removed
133 | if (this.FLAGS != org_app.FLAGS)
134 | {
135 | this.FLAGS |= org_app.FLAGS;
136 | entry.Property(nameof(FLAGS)).IsModified = true;
137 | }
138 | }
139 | if (++count % 100 == 0)
140 | db.SaveChanges();
141 | }
142 |
143 | ///
144 | /// Validate that TITLE_ID matches the expected format.
145 | ///
146 | /// The TITLE_ID string.
147 | /// true if TITLE_ID is valid, false otherwise.
148 | public static bool ValidateTitleID(string title_id)
149 | {
150 | Regex regexp = new Regex(@"^[A-Z]{4}\d{5}$");
151 | if (title_id == null)
152 | return false;
153 | return regexp.IsMatch(title_id);
154 | }
155 |
156 | ///
157 | /// Check if a CONTENT_ID is a Vita title.
158 | ///
159 | /// The CONTENT_ID.
160 | /// True if Vita CONTENT_ID, false otherwise.
161 | public static bool IsVitaContentID(string content_id)
162 | {
163 | if (!ValidateContentID(content_id))
164 | return false;
165 | return (content_id[7] == 'P') || (content_id[7] == 'V');
166 | }
167 |
168 | ///
169 | /// Check if a CONTENT_ID is a Bundle.
170 | ///
171 | /// The CONTENT_ID.
172 | /// True if bundle, false otherwise.
173 | public static bool IsBundleContentID(string content_id)
174 | {
175 | if (!ValidateContentID(content_id))
176 | return false;
177 | return content_id.Substring(7, 4) == "CUSA";
178 | }
179 |
180 | ///
181 | /// Validate that CONTENT_ID matches the expected format.
182 | ///
183 | /// The CONTENT_ID string.
184 | /// true if CONTENT_ID is valid, false otherwise.
185 | public static bool ValidateContentID(string content_id)
186 | {
187 | Regex regexp = new Regex(@"^[A-Z\?]{2}[\d\?]{4}-[A-Z]{4}\d{5}_[\d\?]{2}-[A-Z0-9_\?]{16}$");
188 | if (content_id == null)
189 | return false;
190 | return regexp.IsMatch(content_id);
191 | }
192 |
193 | ///
194 | /// Update or add an App to the DB.
195 | ///
196 | /// The database context.
197 | /// The name the App.
198 | /// The content ID.
199 | /// The content ID of the parent.
200 | /// The category.
201 | /// (Optional) A comment string.
202 | static void AddApp(Database db, string name, string content_id, string parent_id, int? category, string comments = null)
203 | {
204 | if (parent_id == content_id)
205 | parent_id = null;
206 | var app = db.Apps.Find(content_id);
207 | if (app == null)
208 | {
209 | app = new App
210 | {
211 | NAME = name,
212 | TITLE_ID = content_id.Substring(7, 9),
213 | CONTENT_ID = content_id,
214 | PARENT_ID = parent_id,
215 | CATEGORY = category,
216 | COMMENTS = comments
217 | };
218 | }
219 | else
220 | {
221 | app.NAME = name;
222 | app.TITLE_ID = content_id.Substring(7, 9);
223 | app.AddParent(parent_id);
224 | app.CATEGORY = category;
225 | app.COMMENTS = comments;
226 | }
227 | app.SetReadOnly(db, nameof(App.NAME), nameof(App.CATEGORY));
228 | if (comments != null)
229 | app.SetReadOnly(db, nameof(App.COMMENTS));
230 | app.Upsert(db);
231 | }
232 |
233 | ///
234 | /// Update an App entry by querying Chihiro.
235 | ///
236 | /// The database context.
237 | /// (Optional) The language settings to use when querying Chihiro.
238 | /// (Optional) If true, add to the COMMENTS field.
239 | public void UpdateFromChihiro(Database db, string lang = null, bool add_lang = false)
240 | {
241 | var data = Chihiro.GetData(CONTENT_ID, lang);
242 | if (data == null)
243 | return;
244 |
245 | // Process root data
246 | NAME = data.name;
247 | CATEGORY = db.Category[data.top_category];
248 | if (IsVitaContentID(CONTENT_ID))
249 | {
250 | Console.WriteLine($"{TITLE_ID}: {data.name}");
251 | AddApp(db, NAME, CONTENT_ID, PARENT_ID, CATEGORY, add_lang ? lang : null);
252 | }
253 |
254 | // Process entitlements
255 | if (data.default_sku != null)
256 | {
257 | foreach (var ent in Nullable(data.default_sku.entitlements))
258 | {
259 | if (!IsVitaContentID(ent.id) || (ent.id == CONTENT_ID))
260 | continue;
261 | Console.WriteLine($"* {ent.id}: {ent.name}");
262 | AddApp(db, ent.name, ent.id, CONTENT_ID, CATEGORY, add_lang ? lang : null);
263 | }
264 | }
265 |
266 | // Process links
267 | foreach (var link in Nullable(data.links))
268 | {
269 | // May get mixed DLC content
270 | if (!IsVitaContentID(link.id))
271 | continue;
272 | Console.WriteLine($"* {link.id}: {link.top_category}");
273 | AddApp(db, link.name, link.id, CONTENT_ID, db.Category[link.top_category], add_lang ? lang : null);
274 |
275 | // Process sub-entitlements
276 | if (link.default_sku != null)
277 | {
278 | foreach (var ent in Nullable(link.default_sku.entitlements))
279 | {
280 | if (!IsVitaContentID(ent.id) || (ent.id == link.id))
281 | continue;
282 | Console.WriteLine($" * {ent.id}: {ent.name}");
283 | AddApp(db, ent.name, ent.id, CONTENT_ID, db.Category[link.top_category], add_lang ? lang : null);
284 | }
285 | }
286 | }
287 | }
288 | }
289 |
290 | // DB entries for App.CATEGORY
291 | public class Category
292 | {
293 | public string NAME { get; set; }
294 | [Key]
295 | public int VALUE { get; set; }
296 | }
297 |
298 | // DB entries for App.FLAGS
299 | public class Flag
300 | {
301 | public string NAME { get; set; }
302 | [Key]
303 | public int VALUE { get; set; }
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/Json.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * VitaDB - Vita DataBase Updater © 2017 VitaSmith
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | */
9 |
10 | using Newtonsoft.Json;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.Net;
14 |
15 | namespace VitaDB
16 | {
17 | // This is the JSON descriptor for the data we get from Chihiro
18 | public class Chihiro
19 | {
20 | public class SubCategory
21 | {
22 | public string name { get; set; }
23 | public int count { get; set; }
24 | public string key { get; set; }
25 | }
26 |
27 | public class Price
28 | {
29 | public string name { get; set; }
30 | public int count { get; set; }
31 | public string key { get; set; }
32 | }
33 |
34 | public class TopCategory
35 | {
36 | public string name { get; set; }
37 | public int count { get; set; }
38 | public string key { get; set; }
39 | }
40 |
41 | public class Relationship
42 | {
43 | public string name { get; set; }
44 | public int count { get; set; }
45 | public string key { get; set; }
46 | }
47 |
48 | public class Facets
49 | {
50 | public List sub_category { get; set; }
51 | public List price { get; set; }
52 | public List top_category { get; set; }
53 | public List relationship { get; set; }
54 | }
55 |
56 | public class Attributes
57 | {
58 | public Facets facets { get; set; }
59 | public List