() == 143199);
43 | var lastHash = "F369747C6B54914DEAA163AAE85816BA5A8C1845";
44 | Assert.IsTrue(songList.Last().Hash == lastHash);
45 | }
46 |
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 brian91292
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Replacement Released:
2 | [BeatSyncConsole](https://github.com/Zingabopp/BeatSync/releases) is replacing SyncSaberService, and it's finally usable.
3 |
4 | # SyncSaberService
5 | Automatically downloads Beat Saber maps
6 |
7 | Based on SyncSaber by brian, https://github.com/brian91292/SyncSaber/.
8 |
9 | This is currently a standalone application you can run to automatically download maps like the original SyncSaber did.
10 |
11 | # Usage
12 | Extract the zip to a folder of your choosing (avoid the Program Files folders unless you know how to deal with NTFS file permissions). It is recommended to adjust the default configuration in SyncSaberService.ini (to avoid downloading songs you don't want) then run SyncSaberConsole.exe
13 |
14 | # Configuration
15 | The app's settings are in the SyncSaberService.ini file in the same folder as the executable
16 | You must add your BeastSaber username if you want to download your bookmarks and following feeds.
17 | SyncSaberService should automatically find your Beat Saber folder. If it doesn't, you can either manually enter your Beat Saber game's folder or drag and drop the folder onto SyncSaberService.exe to provide your game directory.
18 | SyncSaberService uses the same FavoriteMappers.ini in Beat Saber's UserData folder as the original does. Format is a single mapper's name on each line.
19 |
20 | # Frequenty Asked Question(s)
21 | Why is SyncSaberService skipping songs and not downloading them? When songs are skipped, it's either because SyncSaberService found the songs in your CustomSongs folder or the songs were listed in the SyncSaberHistory.txt located in your Beat Saber UserData folder. The history exists so that SyncSaberService doesn't redownload a song you've previously deleted.
22 |
--------------------------------------------------------------------------------
/Status:
--------------------------------------------------------------------------------
1 | 0.0.1,Broken
2 | 0.0.2,Broken
3 | 0.0.3,Broken
4 | 0.1.0,Broken
5 | 0.1.1,Broken
6 | 0.2.0,Outdated
7 | 0.2.1,Outdated
8 | 0.2.2,Latest
9 |
--------------------------------------------------------------------------------
/SyncSaberConsole/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/SyncSaberConsole/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("SyncSaberConsole")]
9 | [assembly: AssemblyDescription("Automatically download Beat Saber maps.")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("SyncSaberConsole")]
13 | [assembly: AssemblyCopyright("Copyright © Zingabopp 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("9f38d628-2649-4890-86d6-419cc225592c")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("0.2.3")]
36 | [assembly: AssemblyFileVersion("0.2.3")]
37 |
--------------------------------------------------------------------------------
/SyncSaberConsole/SyncSaberConsole.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {9F38D628-2649-4890-86D6-419CC225592C}
8 | Exe
9 | SyncSaberConsole
10 | SyncSaberConsole
11 | v4.7.2
12 | 512
13 | true
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | pdbonly
29 | true
30 | bin\Release\
31 | TRACE
32 | prompt
33 | 4
34 |
35 |
36 | true
37 | bin\x64\Debug\
38 | DEBUG;TRACE
39 | full
40 | x64
41 | prompt
42 | MinimumRecommendedRules.ruleset
43 | true
44 |
45 |
46 | bin\x64\Release\
47 | TRACE
48 | true
49 | pdbonly
50 | x64
51 | prompt
52 | MinimumRecommendedRules.ruleset
53 | true
54 |
55 |
56 | true
57 | bin\x86\Debug\
58 | DEBUG;TRACE
59 | full
60 | x86
61 | prompt
62 | MinimumRecommendedRules.ruleset
63 | true
64 |
65 |
66 | bin\x86\Release\
67 | TRACE
68 | true
69 | pdbonly
70 | x86
71 | prompt
72 | MinimumRecommendedRules.ruleset
73 | true
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 12.0.2
95 |
96 |
97 |
98 |
99 | {47e9e695-c638-4e4f-ad3d-6e8f6abd983f}
100 | SyncSaberLib
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/SyncSaberLib/Config/CustomSetting.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 SyncSaberLib.Config
8 | {
9 | public abstract class CustomSetting
10 | {
11 | private string _name;
12 | public string Name
13 | {
14 | get { return _name; }
15 | set
16 | {
17 | if (string.IsNullOrEmpty(value?.Trim()))
18 | throw new ArgumentException("CustomSetting Name cannot be null or empty.");
19 | _name = value;
20 | }
21 | }
22 | public string Description { get; set; }
23 | public bool Required { get; set; }
24 | public bool Recommended { get; set; }
25 | public abstract object GetValue();
26 | public abstract void SetValue(object value);
27 | }
28 |
29 | public class CustomSetting
30 | : CustomSetting
31 | {
32 | public T Value { get; set; }
33 | public override object GetValue()
34 | {
35 | return Value;
36 | }
37 | public override void SetValue(object value)
38 | {
39 | if (!typeof(T).IsAssignableFrom(value.GetType()))
40 | throw new InvalidCastException($"Cannot convert {value.GetType()} to type {typeof(T).ToString()}.");
41 | Value = (T)value;
42 | }
43 | }
44 |
45 | public static class CustomSettingExtensions
46 | {
47 | public static T Value(this CustomSetting setting)
48 | {
49 | if (!typeof(T).IsAssignableFrom(setting.GetValue().GetType()))
50 | throw new InvalidCastException($"Cannot convert {setting.GetValue().GetType()} to type {typeof(T).ToString()}.");
51 | return (T)setting.GetValue();
52 | }
53 |
54 | public static void AddOrUpdate(this Dictionary dict, CustomSetting setting)
55 | {
56 | if (dict.ContainsKey(setting.Name))
57 | dict[setting.Name].SetValue(setting.GetValue());
58 | else
59 | dict.Add(setting.Name, setting);
60 | }
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/SyncSaberLib/Config/IFeedConfig.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 SyncSaberLib.Config
8 | {
9 | interface IFeedConfig
10 | {
11 | string Name { get; set; }
12 | int FeedIndex { get; set; }
13 | string Description { get; set; }
14 | bool Enabled { get; set; }
15 | int MaxPages { get; set; }
16 | int MaxSongs { get; set; }
17 | int StartingPage { get; set; }
18 | Dictionary CustomSettings { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SyncSaberLib/Config/IReaderConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using FeedReader;
7 |
8 | namespace SyncSaberLib.Config
9 | {
10 | ///
11 | /// Interface for a specific FeedReader configuration.
12 | ///
13 | interface IReaderConfig
14 | {
15 | string ReaderName { get; set; }
16 | string ReaderDescription { get; set; }
17 | bool Enabled { get; set; }
18 | Dictionary AvailableFeeds { get; set; }
19 | Dictionary CustomSettings { get; set; }
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SyncSaberLib/Config/ISyncSaberLibConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 |
8 | /// Songs for SideQuest:
9 | /// AppData\Roaming\SideQuest\bsaber
10 | /// Quest Folder:
11 | /// This PC > Quest > Internal Shared Storage > BeatOnData > CustomSongs
12 | namespace SyncSaberLib.Config
13 | {
14 | ///
15 | /// Overall configuration for SyncSaberLib.
16 | ///
17 | interface ISyncSaberLibConfig
18 | {
19 | IEnumerable FeedConfigs { get; set; }
20 | Dictionary CustomSettings { get; set; }
21 | string ConfigPath { get; }
22 | string ScrapedDataDirectory { get; set; }
23 | string BeatSaberDirectory { get; set; }
24 | string SongDirectoryPath { get; set; }
25 | string LoggingLevel { get; set; }
26 | bool DeleteOldVersions { get; set; }
27 | int DownloadTimeout { get; set; }
28 | int MaxConcurrentDownloads { get; set; }
29 | int MaxConcurrentPageChecks { get; set; }
30 |
31 |
32 | bool SaveChanges();
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/BeatSaverScrape.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.IO;
7 | using System.Reflection;
8 | using Newtonsoft.Json;
9 | using System.Text.RegularExpressions;
10 | using SyncSaberLib.Web;
11 |
12 | namespace SyncSaberLib.Data
13 | {
14 | public class BeatSaverScrape : IScrapedDataModel, BeatSaverSong>
15 | {
16 | private readonly object dataLock = new object();
17 | //[JsonProperty("Data")]
18 | //public List Data { get; private set; }
19 |
20 | public BeatSaverScrape()
21 | {
22 | DefaultPath = Path.Combine(DATA_DIRECTORY.FullName, "BeatSaverScrape.json");
23 | Initialized = false;
24 | Data = new List();
25 |
26 | }
27 |
28 | public override void Initialize(string filePath = "")
29 | {
30 | if (string.IsNullOrEmpty(filePath))
31 | filePath = DefaultPath;
32 |
33 | //(filePath).Populate(this);
34 | if (File.Exists(filePath))
35 | ReadScrapedFile(filePath).Populate(Data);
36 | //JsonSerializer serializer = new JsonSerializer();
37 | //if (test.Type == Newtonsoft.Json.Linq.JTokenType.Array)
38 | // Data = test.ToObject>();
39 | Initialized = true;
40 | CurrentFile = new FileInfo(filePath);
41 | }
42 | /*
43 | public void AddOrUpdate(BeatSaverSong newSong)
44 | {
45 | IEnumerable existing = null;
46 | lock (dataLock)
47 | {
48 | existing = Data.Where(s => s.Equals(newSong));
49 | }
50 | if (existing.Count() > 1)
51 | {
52 | Logger.Warning("Duplicate hash in BeatSaverScrape, this shouldn't happen");
53 | }
54 | if (existing.SingleOrDefault() != null)
55 | {
56 | if (existing.Single().ScrapedAt < newSong.ScrapedAt)
57 | {
58 | lock (dataLock)
59 | {
60 | Data.Remove(existing.Single());
61 | Data.Add(newSong);
62 | }
63 | }
64 | }
65 | }
66 | */
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/IScrapedDataModel.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.IO;
7 | using Newtonsoft.Json;
8 | using Newtonsoft.Json.Linq;
9 | using System.Reflection;
10 |
11 | namespace SyncSaberLib.Data
12 | {
13 | public abstract class IScrapedDataModel
14 | where T : List, new() where DataType : IEquatable
15 | {
16 | private static readonly string ASSEMBLY_PATH = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
17 | public static readonly DirectoryInfo DATA_DIRECTORY = new DirectoryInfo(Path.Combine(ASSEMBLY_PATH, "ScrapedData"));
18 |
19 | [JsonIgnore]
20 | public bool Initialized { get; protected set; }
21 | public bool HasData { get { return Data != null && Data.Count > 0; } }
22 | public virtual T Data { get; protected set; }
23 | [JsonIgnore]
24 | public bool ReadOnly { get; protected set; }
25 | [JsonIgnore]
26 | public string DefaultPath { get; protected set; }
27 | [JsonIgnore]
28 | public FileInfo CurrentFile { get; protected set; }
29 |
30 | public virtual JToken ReadScrapedFile(string filePath)
31 | {
32 | JToken results = null;
33 | string backupPath = filePath + ".bak";
34 | if(File.Exists(backupPath)) // Last write was unsuccessful, delete corrupted file and use the backup
35 | {
36 | if (File.Exists(filePath))
37 | File.Delete(filePath);
38 | File.Move(backupPath, filePath);
39 | }
40 | if (File.Exists(filePath))
41 | using (StreamReader file = File.OpenText(filePath))
42 | {
43 | JsonSerializer serializer = new JsonSerializer();
44 | //results = (JObject)serializer.Deserialize(file, typeof(JObject));
45 | results = JToken.Parse(file.ReadToEnd());
46 | }
47 | return results;
48 | }
49 |
50 | public virtual void WriteFile(string filePath = "")
51 | {
52 | if (string.IsNullOrEmpty(filePath))
53 | filePath = CurrentFile.FullName;
54 | string backupPath = "";
55 | //Backup file before overwriting
56 | if (File.Exists(filePath))
57 | {
58 | backupPath = filePath + ".bak";
59 | File.Move(filePath, backupPath);
60 | }
61 | using (StreamWriter file = File.CreateText(filePath))
62 | {
63 | JsonSerializer serializer = new JsonSerializer();
64 | serializer.Serialize(file, Data);
65 | }
66 | //Write was successful, delete backup
67 | if(!string.IsNullOrEmpty(backupPath) && File.Exists(backupPath))
68 | {
69 | File.Delete(backupPath);
70 | }
71 |
72 | }
73 | public abstract void Initialize(string filePath = "");
74 |
75 |
76 | public virtual bool AddOrUpdate(DataType item, bool exceptionOnNull = false)
77 | {
78 | if (Equals(item, default(DataType)))
79 | if (exceptionOnNull)
80 | throw new ArgumentNullException("Item cannot be null.");
81 | else
82 | return false;
83 | DataType added = default;
84 | DataType removed = default;
85 | bool successful = false;
86 | lock (Data)
87 | {
88 | var match = Data.Where(s => s.Equals(item));
89 | if (match.Count() == 0)
90 | {
91 | //Logger.Debug($"Adding song {song.key} - {song.songName} by {song.authorName} to ScrapedData");
92 | Data.Add(item);
93 | added = item;
94 | successful = true;
95 | }
96 | else
97 | {
98 | removed = match.First();
99 | Data.Remove(removed);
100 | Data.Add(item);
101 | added = item;
102 | successful = true;
103 | }
104 | }
105 | if (!(Equals(added, default(DataType)) || Equals(removed, default(DataType))))
106 | {
107 | CollectionChanged?.Invoke(this, new CollectionChangedEventArgs(added, removed));
108 | }
109 | return successful;
110 | }
111 |
112 | public event EventHandler> CollectionChanged;
113 | }
114 |
115 | public class CollectionChangedEventArgs
116 | where T : IEquatable
117 | {
118 | public T ItemAdded;
119 | public T ItemRemoved;
120 | public CollectionChangedEventArgs(T Added, T Removed)
121 | {
122 | ItemAdded = Added;
123 | ItemRemoved = Removed;
124 | }
125 | }
126 |
127 | public static class JsonExtensions
128 | {
129 | public static void Populate(this JToken value, T target) where T : class
130 | {
131 | using (var sr = value.CreateReader())
132 | {
133 | JsonSerializer.CreateDefault().Populate(sr, target); // Uses the system default JsonSerializerSettings
134 | }
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/JsonConverters.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 | using Newtonsoft.Json.Linq;
8 | using System.Globalization;
9 |
10 | namespace SyncSaberLib.Data
11 | {
12 | // From: https://stackoverflow.com/a/55768479
13 | public class IntegerWithCommasConverter : JsonConverter
14 | {
15 | public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer)
16 | {
17 | if (reader.TokenType == JsonToken.Null)
18 | {
19 | throw new JsonSerializationException("Cannot unmarshal int");
20 | }
21 | if (reader.TokenType == JsonToken.Integer)
22 | return Convert.ToInt32(reader.Value);
23 | var value = (string) reader.Value;
24 | const NumberStyles style = NumberStyles.AllowThousands;
25 | var result = int.Parse(value, style, CultureInfo.InvariantCulture);
26 | return result;
27 | }
28 |
29 | public override void WriteJson(JsonWriter writer, int value, JsonSerializer serializer)
30 | {
31 | writer.WriteValue(value);
32 | }
33 | }
34 |
35 | public class EmptyArrayOrDictionaryConverter : JsonConverter
36 | {
37 | public override bool CanConvert(Type objectType)
38 | {
39 | return objectType.IsAssignableFrom(typeof(Dictionary));
40 | }
41 |
42 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
43 | {
44 | JToken token = JToken.Load(reader);
45 | if (token.Type == JTokenType.Object)
46 | {
47 | return token.ToObject(objectType);
48 | }
49 | else if (token.Type == JTokenType.Array)
50 | {
51 | if (!token.HasValues)
52 | {
53 | // create empty dictionary
54 | return Activator.CreateInstance(objectType);
55 | }
56 | // Handles case where Beat Saver gives the slashstat in the form of an array.
57 | if (objectType == typeof(Dictionary))
58 | {
59 | var retDict = new Dictionary();
60 | for (int i = 0; i < token.Count(); i++)
61 | {
62 | retDict.Add(i.ToString(), (int) token.ElementAt(i));
63 | }
64 | return retDict;
65 | }
66 | }
67 | //throw new JsonSerializationException($"{objectType.ToString()} or empty array expected, received a {token.Type.ToString()}");
68 | Logger.Warning($"{objectType.ToString()} or empty array expected, received a {token.Type.ToString()}");
69 | return Activator.CreateInstance(objectType);
70 | }
71 |
72 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
73 | {
74 | serializer.Serialize(writer, value);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/ScoreSaberScrape.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.IO;
7 | using System.Reflection;
8 | using Newtonsoft.Json;
9 | using System.Text.RegularExpressions;
10 | using SyncSaberLib.Web;
11 |
12 | namespace SyncSaberLib.Data
13 | {
14 | public class ScoreSaberScrape : IScrapedDataModel, ScoreSaberSong>
15 | {
16 | private readonly object dataLock = new object();
17 | public ScoreSaberScrape()
18 | {
19 | Initialized = false;
20 | DefaultPath = Path.Combine(DATA_DIRECTORY.FullName, "ScoreSaberScrape.json");
21 | }
22 |
23 | public override void Initialize(string filePath = "")
24 | {
25 | if (string.IsNullOrEmpty(filePath))
26 | filePath = DefaultPath;
27 | Data = new List();
28 | //(filePath).Populate(this);
29 | if (File.Exists(filePath))
30 | ReadScrapedFile(filePath).Populate(Data);
31 | //JsonSerializer serializer = new JsonSerializer();
32 | //if (test.Type == Newtonsoft.Json.Linq.JTokenType.Array)
33 | // Data = test.ToObject>();
34 | Initialized = true;
35 | CurrentFile = new FileInfo(filePath);
36 | }
37 | /*
38 | public void AddOrUpdate(ScoreSaberSong newSong)
39 | {
40 | IEnumerable existing = null;
41 | lock (dataLock)
42 | {
43 | existing = Data.Where(s => s.Equals(newSong));
44 | }
45 | if (existing.Count() > 1)
46 | {
47 | Logger.Warning("Duplicate hash in BeatSaverScrape, this shouldn't happen");
48 | }
49 | if (existing.SingleOrDefault() != null)
50 | {
51 | if (existing.Single().ScrapedAt < newSong.ScrapedAt)
52 | {
53 | lock (dataLock)
54 | {
55 | Data.Remove(existing.Single());
56 | Data.Add(newSong);
57 | }
58 | }
59 | }
60 | }
61 | */
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/ScoreSaberSong.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 | using System;
4 | using System.Runtime.Serialization;
5 |
6 | namespace SyncSaberLib.Data
7 | {
8 | public class ScoreSaberSong : IEquatable
9 | {
10 | [JsonIgnore]
11 | public bool Populated { get; private set; }
12 |
13 | public ScoreSaberSong()
14 | {
15 |
16 | }
17 |
18 | public static bool TryParseScoreSaberSong(JToken token, ref ScoreSaberSong song)
19 | {
20 | string songName = token["name"]?.Value();
21 | if (songName == null)
22 | songName = "";
23 | bool successful = true;
24 | try
25 | {
26 | song = token.ToObject(new JsonSerializer() {
27 | NullValueHandling = NullValueHandling.Ignore,
28 | MissingMemberHandling = MissingMemberHandling.Ignore
29 | });
30 | //Logger.Debug(song.ToString());
31 | }
32 | catch (Exception ex)
33 | {
34 | Logger.Exception($"Unable to create a ScoreSaberSong from the JSON for {songName}\n", ex);
35 | successful = false;
36 | song = null;
37 | }
38 | return successful;
39 | }
40 | [JsonProperty("uid")]
41 | public int uid { get; set; }
42 | [JsonIgnore]
43 | private string _hash;
44 | ///
45 | /// Hash is always uppercase.
46 | ///
47 | [JsonProperty("id")]
48 | public string hash { get { return _hash; } set { _hash = value.ToUpper(); } }
49 | [JsonProperty("name")]
50 | public string name { get; set; }
51 | [JsonProperty("songSubName")]
52 | public string songSubName { get; set; }
53 | [JsonProperty("songAuthorName")]
54 | public string songAuthorName { get; set; }
55 | [JsonProperty("levelAuthorName")]
56 | public string levelAuthorName { get; set; }
57 |
58 | [JsonProperty("bpm")]
59 | public float bpm { get; set; }
60 |
61 | [JsonProperty("diff")]
62 | private string diff { get; set; }
63 | [JsonIgnore]
64 | public string difficulty
65 | {
66 | get { return ConvertDiff(diff); }
67 | }
68 | [JsonProperty("scores")]
69 | [JsonConverter(typeof(IntegerWithCommasConverter))]
70 | public int scores { get; set; }
71 | [JsonProperty("scores_day")]
72 | public int scores_day { get; set; }
73 | [JsonProperty("ranked")]
74 | public bool ranked { get; set; }
75 | [JsonProperty("stars")]
76 | public float stars { get; set; }
77 | [JsonProperty("image")]
78 | public string image { get; set; }
79 | [JsonProperty("ScrapedAt")]
80 | public DateTime ScrapedAt { get; set; }
81 |
82 | [OnDeserialized]
83 | protected void OnDeserialized(StreamingContext context)
84 | {
85 | //if (!(this is ScoreSaberSong))
86 | //if (!this.GetType().IsSubclassOf(typeof(SongInfo)))
87 | //{
88 | //Logger.Warning("SongInfo OnDeserialized");
89 | Populated = true;
90 | //var song = ScrapedDataProvider.SyncSaberScrape.Where(s => s.hash == md5Hash).FirstOrDefault();
91 | //if (song != null)
92 | // if (song.ScoreSaberInfo.AddOrUpdate(uid, this))
93 | // Logger.Warning($"Adding the same ScoreSaberInfo {uid}-{difficulty} to song {name}");
94 | }
95 |
96 | public SongInfo GenerateSongInfo()
97 | {
98 | var newSong = new SongInfo(hash);
99 | /*
100 | var newSong = new SongInfo() {
101 | songName = name,
102 | songSubName = songSubName,
103 | authorName = levelAuthorName,
104 | bpm = bpm
105 | };
106 | */
107 | //newSong.ScoreSaberInfo.Add(uid, this);
108 | return newSong;
109 | }
110 |
111 | private const string EASYKEY = "_easy_solostandard";
112 | private const string NORMALKEY = "_normal_solostandard";
113 | private const string HARDKEY = "_hard_solostandard";
114 | private const string EXPERTKEY = "_expert_solostandard";
115 | private const string EXPERTPLUSKEY = "_expertplus_solostandard";
116 | public static string ConvertDiff(string diffString)
117 | {
118 | diffString = diffString.ToLower();
119 | if (!diffString.Contains("solostandard"))
120 | return diffString;
121 | switch (diffString)
122 | {
123 | case EXPERTPLUSKEY:
124 | return "ExpertPlus";
125 | case EXPERTKEY:
126 | return "Expert";
127 | case HARDKEY:
128 | return "Hard";
129 | case NORMALKEY:
130 | return "Normal";
131 | case EASYKEY:
132 | return "Easy";
133 | default:
134 | return diffString;
135 | }
136 | }
137 |
138 | public bool Equals(ScoreSaberSong other)
139 | {
140 | return uid == other.uid;
141 | }
142 | }
143 | }
144 |
145 |
146 | //uid 8497
147 | //id "44C9544A577E5B8DC3876F9F696A7F92"
148 | //name "Redo"
149 | //songSubName "Suzuki Konomi"
150 | //author "Splake"
151 | //bpm 190
152 | //diff "_Expert_SoloStandard"
153 | //scores "1,702"
154 | //24hr 8
155 | //ranked 1
156 | //stars 3.03
157 | //image "/imports/images/songs/44C9544A577E5B8DC3876F9F696A7F92.png"
158 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/SongInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using System.Collections;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 | using System.Net.Http;
11 | using System.Net;
12 | using System.Reflection;
13 | using System.Runtime.Serialization;
14 |
15 | namespace SyncSaberLib.Data
16 | {
17 | public class SongInfo : IEquatable
18 | {
19 | // Link: https://raw.githubusercontent.com/andruzzzhka/BeatSaberScrappedData/master/combinedScrappedData.json
20 | private static readonly Regex _digitRegex = new Regex("^[0-9]+$", RegexOptions.Compiled);
21 | private static readonly Regex _beatSaverRegex = new Regex("^[0-9]+-[0-9]+$", RegexOptions.Compiled);
22 | public const char IDENTIFIER_DELIMITER = (char) 0x220E;
23 | private const string DOWNLOAD_URL_BASE = "http://beatsaver.com/download/";
24 |
25 |
26 | [JsonIgnore]
27 | private Dictionary _rankedDiffs;
28 | [JsonIgnore]
29 | public Dictionary RankedDifficulties
30 | {
31 | get
32 | {
33 | if (_rankedDiffs == null)
34 | _rankedDiffs = new Dictionary();
35 | if (ScoreSaberInfo.Count != _rankedDiffs.Count) // If they don't have the same number of difficulties, remake
36 | {
37 | _rankedDiffs = new Dictionary();
38 | foreach (var key in ScoreSaberInfo.Keys)
39 | {
40 | if (ScoreSaberInfo[key].ranked)
41 | {
42 | if (hash == ScoreSaberInfo[key].hash)
43 | _rankedDiffs.AddOrUpdate(ScoreSaberInfo[key].difficulty, ScoreSaberInfo[key].stars);
44 | else
45 | Logger.Debug($"Ranked version of {key} is outdated.\n" +
46 | $" {hash} != {ScoreSaberInfo[key].hash}");
47 | }
48 | }
49 | }
50 | return _rankedDiffs;
51 | }
52 | }
53 |
54 | public BeatSaverSong BeatSaverInfo { get; set; }
55 |
56 | private Dictionary _scoreSaberInfo;
57 | public Dictionary ScoreSaberInfo
58 | {
59 | get
60 | {
61 | if (_scoreSaberInfo == null)
62 | _scoreSaberInfo = new Dictionary();
63 | return _scoreSaberInfo;
64 | }
65 | set { _scoreSaberInfo = value; }
66 | }
67 |
68 | private string _hash;
69 | ///
70 | /// Hash is always uppercase (or empty).
71 | ///
72 | public string hash
73 | {
74 | get
75 | {
76 | if (string.IsNullOrEmpty(_hash))
77 | {
78 | if (BeatSaverInfo != null)
79 | _hash = BeatSaverInfo.hash.ToUpper();
80 | else
81 | {
82 | var ssSong = ScoreSaberInfo.Values.FirstOrDefault();
83 | if (ssSong != null)
84 | _hash = ssSong.hash.ToUpper();
85 | }
86 | }
87 | return _hash;
88 | }
89 | }
90 |
91 | public int keyAsInt
92 | {
93 | get
94 | {
95 | if (BeatSaverInfo != null)
96 | return BeatSaverInfo.KeyAsInt;
97 | return 0;
98 | }
99 | }
100 |
101 | public string key
102 | {
103 | get
104 | {
105 | if (BeatSaverInfo != null)
106 | return BeatSaverInfo.key;
107 | return string.Empty;
108 | }
109 | }
110 |
111 | public string songName
112 | {
113 | get
114 | {
115 | if (BeatSaverInfo != null)
116 | return BeatSaverInfo.metadata.songName;
117 | var ssSong = ScoreSaberInfo.Values.FirstOrDefault();
118 | if (ssSong != null)
119 | return ssSong.name;
120 | return string.Empty;
121 | }
122 | }
123 |
124 | public string authorName
125 | {
126 | get
127 | {
128 | if (BeatSaverInfo != null)
129 | return BeatSaverInfo.uploader.username;
130 | var ssSong = ScoreSaberInfo.Values.FirstOrDefault();
131 | if (ssSong != null)
132 | return ssSong.levelAuthorName;
133 | return string.Empty;
134 | }
135 | }
136 |
137 | public float bpm
138 | {
139 | get
140 | {
141 | if (BeatSaverInfo != null)
142 | return BeatSaverInfo.metadata.bpm;
143 | var ssSong = ScoreSaberInfo.Values.FirstOrDefault();
144 | if (ssSong != null)
145 | return ssSong.bpm;
146 | return 0;
147 | }
148 | }
149 |
150 |
151 | /*
152 | [JsonIgnore]
153 | private string _identifier;
154 | [JsonIgnore]
155 | public string Identifier
156 | {
157 | get
158 | {
159 | if (string.IsNullOrEmpty(_identifier))
160 | {
161 | if (string.IsNullOrEmpty(hash))
162 | return string.Empty;
163 | if (string.IsNullOrEmpty(songName))
164 | return string.Empty;
165 | if (string.IsNullOrEmpty(songSubName))
166 | return string.Empty;
167 | if (string.IsNullOrEmpty(authorName))
168 | return string.Empty;
169 | if (bpm <= 0)
170 | return string.Empty;
171 | _identifier = string.Join(IDENTIFIER_DELIMITER.ToString(), new string[] {
172 | hash,
173 | songName,
174 | songSubName,
175 | authorName,
176 | bpm.ToString()
177 | });
178 | }
179 | return _identifier;
180 | }
181 | }
182 | */
183 | //public SongInfo() { }
184 | public SongInfo(string hash)
185 | {
186 | _hash = hash.ToUpper();
187 | }
188 |
189 | public override string ToString()
190 | {
191 | return hash;
192 | }
193 |
194 | public object this[string propertyName]
195 | {
196 | get
197 | {
198 | Type myType = typeof(SongInfo);
199 | object retVal;
200 | FieldInfo field = myType.GetField(propertyName);
201 | if (field != null)
202 | {
203 | retVal = field.GetValue(this);
204 | }
205 | else
206 | {
207 | PropertyInfo myPropInfo = myType.GetProperty(propertyName);
208 | retVal = myPropInfo.GetValue(this);
209 | }
210 |
211 | Type whatType = retVal.GetType();
212 | return retVal;
213 | }
214 | set
215 | {
216 | Type myType = typeof(SongInfo);
217 | PropertyInfo myPropInfo = myType.GetProperty(propertyName);
218 | myPropInfo.SetValue(this, value, null);
219 | }
220 | }
221 |
222 | public bool Equals(SongInfo other)
223 | {
224 | return hash.ToUpper() == other.hash.ToUpper();
225 | }
226 | }
227 |
228 |
229 |
230 | }
231 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/SongInfoProvider.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 SyncSaberLib.Data
8 | {
9 | public static class SongInfoProvider
10 | {
11 | public static BeatSaverScrape BeatSaverSongs { get; set; }
12 | public static ScoreSaberScrape ScoreSaberSongs { get; set; }
13 |
14 | public static void Initialize()
15 | {
16 |
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SyncSaberLib/Data/SyncSaberScrape.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.IO;
7 | using System.Reflection;
8 | using Newtonsoft.Json;
9 | using System.Text.RegularExpressions;
10 | using SyncSaberLib.Web;
11 |
12 | namespace SyncSaberLib.Data
13 | {
14 | [Obsolete("Replacing with separate classes for Beat Saver and ScoreSaber data")]
15 | public class SyncSaberScrape : IScrapedDataModel, SongInfo>
16 | {
17 | private static readonly string ASSEMBLY_PATH = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
18 |
19 | public SyncSaberScrape()
20 | {
21 | Data = new List();
22 | DefaultPath = Path.Combine(DATA_DIRECTORY.FullName, "SyncSaberScrapedData.json");
23 | }
24 |
25 | public override void Initialize(string filePath = "")
26 | {
27 | if (string.IsNullOrEmpty(filePath))
28 | filePath = DefaultPath;
29 | Data = new List();
30 | //(filePath).Populate(this);
31 | if(File.Exists(filePath))
32 | ReadScrapedFile(filePath).Populate(this);
33 | //JsonSerializer serializer = new JsonSerializer();
34 | //if (test.Type == Newtonsoft.Json.Linq.JTokenType.Array)
35 | // Data = test.ToObject>();
36 | Initialized = true;
37 | CurrentFile = new FileInfo(filePath);
38 | }
39 |
40 |
41 | public override void WriteFile(string filePath = "")
42 | {
43 | if (string.IsNullOrEmpty(filePath))
44 | filePath = CurrentFile.FullName;
45 | using (StreamWriter file = File.CreateText(filePath))
46 | {
47 | JsonSerializer serializer = new JsonSerializer();
48 | serializer.Serialize(file, this);
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/SyncSaberLib/Playlist.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.IO;
7 | using Newtonsoft.Json;
8 | using Newtonsoft.Json.Linq;
9 |
10 | namespace SyncSaberLib
11 | {
12 |
13 | [Serializable]
14 | public class Playlist
15 | {
16 | public Playlist(string playlistFileName, string playlistTitle, string playlistAuthor, string image)
17 | {
18 | fileName = playlistFileName;
19 | Title = playlistTitle;
20 | Author = playlistAuthor;
21 | Image = image;
22 | Songs = new List();
23 | fileLoc = "";
24 | ReadPlaylist();
25 | }
26 |
27 | public void TryAdd(string songHash, string songIndex, string songName)
28 | {
29 | if (!Songs.Exists(s => !string.IsNullOrEmpty(s.hash) && s.hash.ToUpper() == songHash.ToUpper()))
30 | {
31 | Songs.Add(new PlaylistSong(songHash, songIndex, songName));
32 | // Remove any duplicate song that doesn't have a hash
33 | var oldSongs = Songs.Where(s => string.IsNullOrEmpty(s.hash) && !string.IsNullOrEmpty(s.key) && s.key.ToLower() == songIndex.ToLower()).ToArray();
34 | foreach (var song in oldSongs)
35 | {
36 | Songs.Remove(song);
37 | }
38 | }
39 | }
40 |
41 | public void WritePlaylist()
42 | {
43 | PlaylistIO.WritePlaylist(this);
44 | }
45 |
46 | public bool ReadPlaylist()
47 | {
48 | string oldFormatPath = Path.Combine(OldConfig.BeatSaberPath, "Playlists", fileName + ".json");
49 | string newFormatPath = Path.Combine(OldConfig.BeatSaberPath, "Playlists", fileName + ".bplist");
50 | oldFormat = !File.Exists(newFormatPath);
51 | Logger.Info($"Playlist {Title} found in {(oldFormat ? "old" : "new")} playlist format.");
52 | if (File.Exists(oldFormat ? oldFormatPath : newFormatPath))
53 | {
54 |
55 | PlaylistIO.ReadPlaylistSongs(this);
56 | /*
57 | if (playlist != null)
58 | {
59 | Title = playlist.Title;
60 | Author = playlist.Author;
61 | Image = playlist.Image;
62 | Songs = playlist.Songs;
63 | fileLoc = playlist.fileLoc;
64 | Logger.Info("Success loading playlist!");
65 | return true;
66 | }*/
67 | }
68 | return false;
69 | }
70 |
71 | [JsonProperty("playlistTitle")]
72 | public string Title;
73 | [JsonProperty("playlistAuthor")]
74 | public string Author;
75 |
76 | [JsonProperty("image")]
77 | public string Image;
78 |
79 | [JsonProperty("songs")]
80 | public List Songs;
81 |
82 | [JsonProperty("fileLoc")]
83 | public string fileLoc;
84 | [JsonIgnore]
85 | public string fileName;
86 | [JsonIgnore]
87 | public bool oldFormat = true;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/SyncSaberLib/PlaylistIO.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | //using SimpleJSON;
7 | using System.IO;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 |
11 |
12 | namespace SyncSaberLib
13 | {
14 | class PlaylistIO
15 | {
16 | public static Playlist ReadPlaylistSongs(Playlist playlist)
17 | {
18 | try
19 | {
20 | string filePath = Path.Combine(OldConfig.BeatSaberPath, "Playlists", playlist.fileName + (playlist.oldFormat ? ".json" : ".bplist"));
21 | //var playListJson = JObject.Parse(File.ReadAllText(filePath));
22 | JsonConvert.PopulateObject(File.ReadAllText(filePath), playlist);
23 | playlist.fileLoc = null;
24 |
25 | return playlist;
26 | }
27 | catch (Exception ex)
28 | {
29 | Logger.Exception("Exception parsing playlist:", ex);
30 | }
31 | return null;
32 | }
33 |
34 | public static void WritePlaylist(Playlist playlist)
35 | {
36 |
37 | if (!Directory.Exists(Path.Combine(OldConfig.BeatSaberPath, "Playlists")))
38 | {
39 | Directory.CreateDirectory(Path.Combine(OldConfig.BeatSaberPath, "Playlists"));
40 | }
41 | var jsonString = JsonConvert.SerializeObject(playlist);
42 | File.WriteAllText(Path.Combine(OldConfig.BeatSaberPath, "Playlists", playlist.fileName + (playlist.oldFormat ? ".json" : ".bplist")), jsonString);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SyncSaberLib/PlaylistSong.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 | using Newtonsoft.Json.Linq;
8 |
9 | namespace SyncSaberLib
10 | {
11 | [Serializable]
12 | public class PlaylistSong
13 | {
14 | public PlaylistSong(string _hash, string _songIndex, string _songName)
15 | {
16 | hash = _hash;
17 | key = _songIndex;
18 | songName = _songName;
19 | }
20 |
21 | [JsonProperty("key")]
22 | public string key;
23 |
24 | [JsonProperty("hash")]
25 | public string hash;
26 |
27 | [JsonProperty("songName")]
28 | public string songName;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SyncSaberLib/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("SyncSaberLib")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("SyncSaberLib")]
13 | [assembly: AssemblyCopyright("Copyright © 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("47e9e695-c638-4e4f-ad3d-6e8f6abd983f")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("0.2.2")]
36 | [assembly: AssemblyFileVersion("0.2.2")]
37 |
--------------------------------------------------------------------------------
/SyncSaberLib/SyncSaberLib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}
8 | Library
9 | SyncSaberLib
10 | SyncSaberLib
11 | v4.7.2
12 | 512
13 | true
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | none
29 | true
30 | bin\Release\
31 |
32 |
33 | prompt
34 | 4
35 | true
36 |
37 |
38 |
39 |
40 |
41 |
42 | true
43 | bin\x64\Debug\
44 | DEBUG;TRACE
45 | full
46 | x64
47 | prompt
48 | MinimumRecommendedRules.ruleset
49 |
50 |
51 | bin\x64\Release\
52 | true
53 | x64
54 | prompt
55 | MinimumRecommendedRules.ruleset
56 | true
57 |
58 |
59 | true
60 | bin\x86\Debug\
61 | DEBUG;TRACE
62 | full
63 | x86
64 | prompt
65 | MinimumRecommendedRules.ruleset
66 |
67 |
68 | bin\x86\Release\
69 | true
70 | x86
71 | prompt
72 | MinimumRecommendedRules.ruleset
73 | true
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 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | 2.5.2
121 |
122 |
123 | 12.0.2
124 |
125 |
126 | 4.9.0
127 |
128 |
129 |
130 |
131 | {ada8c603-a103-4324-a308-6d4236270c13}
132 | FeedReader
133 |
134 |
135 | {76dea3a1-3558-4dfe-834b-1fbd597a4dd6}
136 | BeatSaber-PlayerDataReader
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/SyncSaberLib/Utilities.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Concurrent;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using IniParser;
9 | using IniParser.Model;
10 | using System.IO;
11 | using System.IO.Compression;
12 | using System.Net;
13 | using System.ComponentModel;
14 | using System.Diagnostics;
15 |
16 | namespace SyncSaberLib
17 | {
18 | public static class Utilities
19 | {
20 | public static string MakeSafeFilename(string str)
21 | {
22 | StringBuilder retStr = new StringBuilder(str);
23 | foreach (var character in Path.GetInvalidFileNameChars())
24 | {
25 | retStr.Replace(character.ToString(), string.Empty);
26 | }
27 | return retStr.ToString();
28 | }
29 |
30 |
31 |
32 | ///
33 | /// Tries to parse a string as a bool, returns false if it fails.
34 | ///
35 | ///
36 | ///
37 | ///
38 | /// Successful
39 | public static bool StrToBool(string str, out bool result, bool defaultVal = false)
40 | {
41 | bool successful = true;
42 | switch (str.ToLower())
43 | {
44 | case "0":
45 | result = false;
46 | break;
47 | case "false":
48 | result = false;
49 | break;
50 | case "1":
51 | result = true;
52 | break;
53 | case "true":
54 | result = true;
55 | break;
56 | default:
57 | successful = false;
58 | result = defaultVal;
59 | break;
60 | }
61 | return successful;
62 | }
63 |
64 |
65 |
66 | public static void EmptyDirectory(string directory, bool delete = true)
67 | {
68 | DirectoryInfo directoryInfo = new DirectoryInfo(directory);
69 | if (directoryInfo.Exists)
70 | {
71 | foreach (FileInfo file in directoryInfo.GetFiles())
72 | {
73 | file.Delete();
74 | }
75 | foreach (DirectoryInfo dir in directoryInfo.GetDirectories())
76 | {
77 | dir.Delete(true);
78 | }
79 | if (delete)
80 | {
81 | directoryInfo.Delete(true);
82 | }
83 | }
84 | }
85 |
86 | public static void MoveFilesRecursively(DirectoryInfo source, DirectoryInfo target)
87 | {
88 | foreach (DirectoryInfo directoryInfo in source.GetDirectories())
89 | {
90 | Utilities.MoveFilesRecursively(directoryInfo, target.CreateSubdirectory(directoryInfo.Name));
91 | }
92 | foreach (FileInfo fileInfo in source.GetFiles())
93 | {
94 | string newPath = Path.Combine(target.FullName, fileInfo.Name);
95 | if (File.Exists(newPath))
96 | {
97 | try
98 | {
99 | File.Delete(newPath);
100 | }
101 | catch (Exception)
102 | {
103 | try
104 | {
105 | string oldFilePath = Path.Combine(target.FullName, "FilesToDelete");
106 | if (!Directory.Exists(oldFilePath))
107 | {
108 | Directory.CreateDirectory(oldFilePath);
109 | }
110 | File.Move(newPath, Path.Combine(oldFilePath, fileInfo.Name));
111 | }
112 | catch (Exception) { } // TODO: This is dirty code
113 | }
114 | }
115 | // Check for file lock
116 | var time = Stopwatch.StartNew();
117 | bool waitTimeout = false;
118 | while (IsFileLocked(fileInfo.FullName) && !waitTimeout)
119 | waitTimeout = time.ElapsedMilliseconds < 1000;
120 | if (waitTimeout)
121 | Logger.Warning($"Timeout waiting for {fileInfo.FullName} to be released to move.");
122 | fileInfo.MoveTo(newPath);
123 | }
124 | }
125 | //async static Task
126 | public static bool IsFileLocked(string filename)
127 | {
128 | // If the file can be opened for exclusive access it means that the file
129 | // is no longer locked by another process.
130 | try
131 | {
132 | using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
133 | return !(inputStream.Length > 0);
134 | }
135 | catch (Exception) // TODO: Catch only IOException? so the caller doesn't wait for a Timeout if there are other errors
136 | {
137 | return true;
138 | }
139 | }
140 |
141 | public static void WriteStringListSafe(string path, List data, bool sort = true)
142 | {
143 | if (File.Exists(path))
144 | {
145 | File.Copy(path, path + ".bak", true);
146 | }
147 | if (sort)
148 | {
149 | data.Sort();
150 | }
151 | File.WriteAllLines(path, data);
152 | File.Delete(path + ".bak");
153 | }
154 |
155 | public static string FormatTimeSpan(TimeSpan timeElapsed)
156 | {
157 | string timeElapsedStr = "";
158 | if (timeElapsed.TotalMinutes >= 1)
159 | {
160 | timeElapsedStr = $"{(int) timeElapsed.TotalMinutes}m ";
161 | }
162 | timeElapsedStr = $"{timeElapsedStr}{timeElapsed.Seconds}s";
163 | return timeElapsedStr;
164 | }
165 | }
166 |
167 | internal static class ConcurrentQueueExtensions
168 | {
169 | public static void Clear(this ConcurrentQueue queue)
170 | {
171 | while (queue.TryDequeue(out T item))
172 | {
173 | // do nothing
174 | }
175 | }
176 | }
177 |
178 | public static class AggregateExceptionExtensions
179 | {
180 | public static void WriteExceptions(this AggregateException ae, string message)
181 | {
182 | for(int i = 0; i < ae.InnerExceptions.Count; i++)
183 | {
184 | Logger.Exception($"Exception {i}:\n", ae.InnerExceptions[i]);
185 | if (ae.InnerExceptions[i] is AggregateException ex)
186 | WriteExceptions(ex, ""); // TODO: This could get very long
187 | }
188 | }
189 | }
190 |
191 | internal static class DictionaryExtensions
192 | {
193 | ///
194 | /// Adds the given key and value to the dictionary. If they key already exists, updates the value.
195 | /// Returns true if the key already exists.
196 | ///
197 | ///
198 | ///
199 | ///
200 | ///
201 | ///
202 | /// True if the key already exists, false otherwise.
203 | public static bool AddOrUpdate(this Dictionary dict, TKey key, TValue value)
204 | {
205 | if (dict.ContainsKey(key))
206 | {
207 | dict[key] = value;
208 | return true;
209 | }
210 | dict.Add(key, value);
211 | return false;
212 | }
213 | }
214 |
215 | }
216 |
--------------------------------------------------------------------------------
/SyncSaberLib/Web/DownloadBatch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using System.Threading.Tasks.Dataflow;
8 | using static SyncSaberLib.Utilities;
9 | using SyncSaberLib.Data;
10 |
11 | namespace SyncSaberLib.Web
12 | {
13 | class DownloadBatch
14 | {
15 | public async Task WorkDownloadQueueAsync()
16 | {
17 | BatchComplete = false;
18 | int maxConcurrentDownloads = OldConfig.MaxConcurrentDownloads; // Set it here so it doesn't error
19 | var actionBlock = new ActionBlock(job => {
20 | Logger.Debug($"Running job {job.Song.key} in ActionBlock");
21 | Task newTask = job.RunJobAsync();
22 | try
23 | {
24 | newTask.Wait();
25 | }
26 | catch (AggregateException ae)
27 | {
28 | foreach (var ex in ae.InnerExceptions)
29 | {
30 | Logger.Exception($"Error while running job {job.Song.key}-{job.Song.songName} by {job.Song.authorName}\n", ex);
31 | }
32 | }
33 | finally
34 | {
35 | TaskComplete(job.Song, job);
36 | }
37 | }, new ExecutionDataflowBlockOptions {
38 | BoundedCapacity = 500,
39 | MaxDegreeOfParallelism = maxConcurrentDownloads
40 | });
41 | while (_songDownloadQueue.Count > 0)
42 | {
43 | var job = _songDownloadQueue.Pop();
44 | Logger.Trace($"Adding job for {job.Song.key}");
45 | await actionBlock.SendAsync(job).ConfigureAwait(false);
46 | }
47 |
48 | actionBlock.Complete();
49 | await actionBlock.Completion.ConfigureAwait(false);
50 | Logger.Trace($"Actionblock complete");
51 | BatchComplete = true;
52 | }
53 |
54 | public void TaskComplete(SongInfo song, DownloadJob job)
55 | {
56 | bool successful = job.Result == DownloadJob.JobResult.SUCCESS;
57 | //string failReason;
58 | switch (job.Result)
59 | {
60 | case DownloadJob.JobResult.SUCCESS:
61 | Logger.Info($"Finished job {song.key}-{song.songName} by {song.authorName} successfully.");
62 | Logger.Info($"Song {song.key} downloaded to {job.SongDirectory.FullName}");
63 | break;
64 | case DownloadJob.JobResult.TIMEOUT:
65 | Logger.Warning($"Job {song.key} failed due to download timeout.");
66 | break;
67 | case DownloadJob.JobResult.NOTFOUND:
68 | Logger.Warning($"Job failed, {song.key} could not be found on Beat Saver."); // TODO: Put song in history so we don't try to download it again.
69 | break;
70 | case DownloadJob.JobResult.UNZIPFAILED:
71 | Logger.Warning($"Job failed, {song.key} failed during unzipping.");
72 | break;
73 | case DownloadJob.JobResult.OTHERERROR:
74 | Logger.Warning($"Job {song.key} failed for...reasons.");
75 | break;
76 | default:
77 | break;
78 | }
79 |
80 |
81 | JobCompleted(job);
82 | }
83 |
84 | public async Task RunJobs()
85 | {
86 | await WorkDownloadQueueAsync().ConfigureAwait(false);
87 | }
88 |
89 | public void AddJob(DownloadJob job)
90 | {
91 | if (_songDownloadQueue.Where(j => j.Song.key == job.Song.key).Count() == 0)
92 | _songDownloadQueue.Push(job);
93 | else
94 | Logger.Warning($"{job.Song.key} is already in the queue");
95 | }
96 |
97 | public event Action JobCompleted;
98 |
99 | private Stack _songDownloadQueue = new Stack();
100 | public bool BatchComplete { get; private set; } = false;
101 |
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/SyncSaberLib/Web/IFeedReader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 | using SyncSaberLib.Data;
4 | using System.Linq;
5 |
6 | namespace SyncSaberLib.Web
7 | {
8 | public interface IFeedReader
9 | {
10 | string Name { get; } // Name of the reader
11 | string Source { get; } // Name of the site
12 | bool Ready { get; } // Reader is ready
13 |
14 | ///
15 | /// Anything that needs to happen before the Reader is ready.
16 | ///
17 | void PrepareReader();
18 |
19 | ///
20 | /// Retrieves the songs from a feed and returns them as a Dictionary. Key is the Beat Saver key as an integer.
21 | ///
22 | ///
23 | ///
24 | Dictionary GetSongsFromFeed(IFeedSettings settings);
25 |
26 | ///
27 | /// Gets the playlists associated with the feed.
28 | ///
29 | ///
30 | ///
31 | Playlist[] PlaylistsForFeed(int feedIndex);
32 | }
33 |
34 | public interface IFeedSettings
35 | {
36 | string FeedName { get; } // Name of the feed
37 | int FeedIndex { get; } // Index of the feed
38 | int MaxSongs { get; set; } // Max number of songs to retrieve
39 | bool searchOnline { get; set; } // Search online instead of using local scrapes
40 | bool UseSongKeyAsOutputFolder { get; set; } // Use the song key as the output folder name instead of the default
41 | }
42 |
43 | ///
44 | /// Data for a feed.
45 | ///
46 | public struct FeedInfo
47 | {
48 | public FeedInfo(string _name, string _baseUrl)
49 | {
50 | Name = _name;
51 | BaseUrl = _baseUrl;
52 | }
53 | public string BaseUrl; // Base URL for the feed, has string keys to replace with things like page number/bsaber username
54 | public string Name; // Name of the feed
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/SyncSaberService.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29102.190
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncSaberLib", "SyncSaberLib\SyncSaberLib.csproj", "{47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncSaberServiceTests", "SyncSaberServiceTests\SyncSaberServiceTests.csproj", "{15C81339-6560-4DB8-AEC1-9E6C32678935}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncSaberConsole", "SyncSaberConsole\SyncSaberConsole.csproj", "{9F38D628-2649-4890-86D6-419CC225592C}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeatSaber-PlayerDataReader", "SyncSaberLib\libs\BeatSaber-PlayerDataReader\BeatSaber-PlayerDataReader\BeatSaber-PlayerDataReader.csproj", "{76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeedReader", "FeedReader\FeedReader.csproj", "{ADA8C603-A103-4324-A308-6D4236270C13}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeedReaderTests", "FeedReaderTests\FeedReaderTests.csproj", "{A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUtilities", "WebUtilities\WebUtilities.csproj", "{1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}"
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Debug|x64 = Debug|x64
24 | Debug|x86 = Debug|x86
25 | Release|Any CPU = Release|Any CPU
26 | Release|x64 = Release|x64
27 | Release|x86 = Release|x86
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|x64.ActiveCfg = Debug|x64
33 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|x64.Build.0 = Debug|x64
34 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|x86.ActiveCfg = Debug|x86
35 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Debug|x86.Build.0 = Debug|x86
36 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|x64.ActiveCfg = Release|x64
39 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|x64.Build.0 = Release|x64
40 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|x86.ActiveCfg = Release|x86
41 | {47E9E695-C638-4E4F-AD3D-6E8F6ABD983F}.Release|x86.Build.0 = Release|x86
42 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Debug|x64.ActiveCfg = Debug|Any CPU
45 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Debug|x86.ActiveCfg = Debug|Any CPU
46 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Release|x64.ActiveCfg = Release|Any CPU
49 | {15C81339-6560-4DB8-AEC1-9E6C32678935}.Release|x86.ActiveCfg = Release|Any CPU
50 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|x64.ActiveCfg = Debug|x64
53 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|x64.Build.0 = Debug|x64
54 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|x86.ActiveCfg = Debug|x86
55 | {9F38D628-2649-4890-86D6-419CC225592C}.Debug|x86.Build.0 = Debug|x86
56 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|Any CPU.Build.0 = Release|Any CPU
58 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|x64.ActiveCfg = Release|x64
59 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|x64.Build.0 = Release|x64
60 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|x86.ActiveCfg = Release|x86
61 | {9F38D628-2649-4890-86D6-419CC225592C}.Release|x86.Build.0 = Release|x86
62 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
64 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|x64.ActiveCfg = Debug|Any CPU
65 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|x64.Build.0 = Debug|Any CPU
66 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|x86.ActiveCfg = Debug|Any CPU
67 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Debug|x86.Build.0 = Debug|Any CPU
68 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|x64.ActiveCfg = Release|Any CPU
71 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|x64.Build.0 = Release|Any CPU
72 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|x86.ActiveCfg = Release|Any CPU
73 | {76DEA3A1-3558-4DFE-834B-1FBD597A4DD6}.Release|x86.Build.0 = Release|Any CPU
74 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|x64.ActiveCfg = Debug|Any CPU
77 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|x64.Build.0 = Debug|Any CPU
78 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|x86.ActiveCfg = Debug|Any CPU
79 | {ADA8C603-A103-4324-A308-6D4236270C13}.Debug|x86.Build.0 = Debug|Any CPU
80 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|Any CPU.ActiveCfg = Release|Any CPU
81 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|Any CPU.Build.0 = Release|Any CPU
82 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|x64.ActiveCfg = Release|Any CPU
83 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|x64.Build.0 = Release|Any CPU
84 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|x86.ActiveCfg = Release|Any CPU
85 | {ADA8C603-A103-4324-A308-6D4236270C13}.Release|x86.Build.0 = Release|Any CPU
86 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
87 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|Any CPU.Build.0 = Debug|Any CPU
88 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|x64.ActiveCfg = Debug|Any CPU
89 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|x64.Build.0 = Debug|Any CPU
90 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|x86.ActiveCfg = Debug|Any CPU
91 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Debug|x86.Build.0 = Debug|Any CPU
92 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|Any CPU.ActiveCfg = Release|Any CPU
93 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|Any CPU.Build.0 = Release|Any CPU
94 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|x64.ActiveCfg = Release|Any CPU
95 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|x64.Build.0 = Release|Any CPU
96 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|x86.ActiveCfg = Release|Any CPU
97 | {A1BE9AFD-24A9-42B4-81E4-83433E1E1C65}.Release|x86.Build.0 = Release|Any CPU
98 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
99 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
100 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|x64.ActiveCfg = Debug|Any CPU
101 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|x64.Build.0 = Debug|Any CPU
102 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|x86.ActiveCfg = Debug|Any CPU
103 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Debug|x86.Build.0 = Debug|Any CPU
104 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
105 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|Any CPU.Build.0 = Release|Any CPU
106 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|x64.ActiveCfg = Release|Any CPU
107 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|x64.Build.0 = Release|Any CPU
108 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|x86.ActiveCfg = Release|Any CPU
109 | {1578AEC5-0D16-45F8-8987-AE23FFFE1FFA}.Release|x86.Build.0 = Release|Any CPU
110 | EndGlobalSection
111 | GlobalSection(SolutionProperties) = preSolution
112 | HideSolutionNode = FALSE
113 | EndGlobalSection
114 | GlobalSection(ExtensibilityGlobals) = postSolution
115 | SolutionGuid = {A40DEEC2-9B61-4EA2-A034-FB7539DFD704}
116 | EndGlobalSection
117 | EndGlobal
118 |
--------------------------------------------------------------------------------
/SyncSaberServiceTests/Data/BeatSaverSongTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using Newtonsoft.Json.Linq;
3 | using SyncSaberLib.Data;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace SyncSaberLib.Data.Tests
12 | {
13 | [TestClass()]
14 | public class BeatSaverSongTests
15 | {
16 | [TestMethod()]
17 | public void TryParseBeatSaverTest()
18 | {
19 | string jsonStr = File.ReadAllText("test_detail_page.json");
20 | JToken singleSong = JToken.Parse(jsonStr);
21 | bool successful = BeatSaverSong.TryParseBeatSaver(singleSong, out BeatSaverSong song);
22 | Console.WriteLine($"{song.name}");
23 | Assert.IsTrue(successful);
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/SyncSaberServiceTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("SyncSaberServiceTests")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("SyncSaberServiceTests")]
13 | [assembly: AssemblyCopyright("Copyright © 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("15c81339-6560-4db8-aec1-9e6c32678935")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/SyncSaberServiceTests/SongInfoTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using SyncSaberLib;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using SyncSaberLib.Data;
9 |
10 | namespace SyncSaberLib.Tests
11 | {
12 | [TestClass()]
13 | public class SongInfoTests
14 | {
15 | [TestMethod()]
16 | public void SongInfoTest_ValidID()
17 | {
18 | //SongInfo testSong = new SongInfo("1234-23", "Test Song", "http://testurl.com", "TestAuthor");
19 | //Assert.IsTrue(testSong.keyAsInt == 1234);
20 | //Assert.IsTrue(testSong.SongVersion == 23);
21 | }
22 |
23 | [TestMethod()]
24 | public void SongInfoTest_InvalidID_SingleNum()
25 | {
26 | //SongInfo testSong = new SongInfo("1234", "Test Song", "http://testurl.com", "TestAuthor");
27 | //Assert.IsTrue(testSong.keyAsInt == 0);
28 | //Assert.IsTrue(testSong.SongVersion == 0);
29 | }
30 |
31 | [TestMethod()]
32 | public void SongInfoTest_InvalidID_HasLetters()
33 | {
34 | //SongInfo testSong = new SongInfo("1234s-23", "Test Song", "http://testurl.com", "TestAuthor");
35 | //Assert.IsTrue(testSong.keyAsInt == 0);
36 | //Assert.IsTrue(testSong.SongVersion == 0);
37 | }
38 |
39 | [TestMethod()]
40 | public void SongInfoTest_InvalidID_EmptyString()
41 | {
42 | //SongInfo testSong = new SongInfo("", "Test Song", "http://testurl.com", "TestAuthor");
43 | //Assert.IsTrue(testSong.keyAsInt == 0);
44 | //Assert.IsTrue(testSong.SongVersion == 0);
45 | }
46 |
47 | [TestMethod()]
48 | public void SongInfoTest_InvalidID_NoID()
49 | {
50 | //SongInfo testSong = new SongInfo("-123", "Test Song", "http://testurl.com", "TestAuthor");
51 | //Assert.IsTrue(testSong.keyAsInt == 0);
52 | //Assert.IsTrue(testSong.SongVersion == 0);
53 | }
54 |
55 | [TestMethod()]
56 | public void SongInfoTest_InvalidID_NoVersion()
57 | {
58 | //SongInfo testSong = new SongInfo("1234-", "Test Song", "http://testurl.com", "TestAuthor");
59 | //Assert.IsTrue(testSong.keyAsInt == 0);
60 | //Assert.IsTrue(testSong.SongVersion == 0);
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/SyncSaberServiceTests/SyncSaberServiceTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {15C81339-6560-4DB8-AEC1-9E6C32678935}
8 | Library
9 | Properties
10 | SyncSaberServiceTests
11 | SyncSaberServiceTests
12 | v4.7.2
13 | 512
14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 10.0
16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
18 | False
19 | UnitTest
20 |
21 |
22 |
23 |
24 |
25 | true
26 | full
27 | false
28 | bin\Debug\
29 | DEBUG;TRACE
30 | prompt
31 | 4
32 |
33 |
34 | pdbonly
35 | true
36 | bin\Release\
37 | TRACE
38 | prompt
39 | 4
40 |
41 |
42 |
43 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
44 |
45 |
46 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
47 |
48 |
49 | ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | {47e9e695-c638-4e4f-ad3d-6e8f6abd983f}
72 | SyncSaberLib
73 |
74 |
75 |
76 |
77 | Always
78 |
79 |
80 | Always
81 |
82 |
83 |
84 |
85 |
86 |
87 | False
88 |
89 |
90 | False
91 |
92 |
93 | False
94 |
95 |
96 | False
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
106 |
107 |
108 |
109 |
110 |
111 |
118 |
--------------------------------------------------------------------------------
/SyncSaberServiceTests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/SyncSaberServiceTests/test_detail_page.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 |
4 | "metadata": {
5 | "difficulties": {
6 | "easy": true,
7 | "expert": true,
8 | "expertPlus": true,
9 | "hard": true,
10 | "normal": true
11 | },
12 | "characteristics": [
13 | {
14 | "name": "Standard",
15 | "difficulties": {
16 | "easy": {
17 | "duration": 511,
18 | "length": 180,
19 | "bombs": 0,
20 | "notes": 198,
21 | "obstacles": 23,
22 | "njs": 11
23 | },
24 | "normal": {
25 | "duration": 511,
26 | "length": 180,
27 | "bombs": 0,
28 | "notes": 280,
29 | "obstacles": 25,
30 | "njs": 11
31 | },
32 | "hard": {
33 | "duration": 511,
34 | "length": 180,
35 | "bombs": 0,
36 | "notes": 382,
37 | "obstacles": 20,
38 | "njs": 12
39 | },
40 | "expert": {
41 | "duration": 511,
42 | "length": 180,
43 | "bombs": 0,
44 | "notes": 533,
45 | "obstacles": 20,
46 | "njs": 14
47 | },
48 | "expertPlus": {
49 | "duration": 511,
50 | "length": 180,
51 | "bombs": 2,
52 | "notes": 704,
53 | "obstacles": 25,
54 | "njs": 15
55 | }
56 | }
57 | },
58 | {
59 | "name": "OneSaber",
60 | "difficulties": {
61 | "easy": null,
62 | "normal": null,
63 | "hard": null,
64 | "expert": {
65 | "duration": 511,
66 | "length": 180,
67 | "bombs": 0,
68 | "notes": 450,
69 | "obstacles": 44,
70 | "njs": 14
71 | },
72 | "expertPlus": null
73 | }
74 | }
75 | ],
76 | "levelAuthorName": "Skyler Wallace",
77 | "songAuthorName": "Urban Cone",
78 | "songName": "Come Back To Me",
79 | "songSubName": "ft. Tove Lo",
80 | "bpm": 170
81 | },
82 | "stats": {
83 | "downloads": 3379,
84 | "plays": 0,
85 | "downVotes": 1,
86 | "upVotes": 57,
87 | "heat": 790.2998286,
88 | "rating": 0.8412931449578613
89 | },
90 | "description": "170 BPM / 3:06 Runtime\nEasy - 198 Notes\nNormal - 280 Notes\nHard - 382 Notes\nExpert - 533 Notes\nExpert (Single Saber) / 450 Notes\nExpert+ / 704 Notes",
91 | "deletedAt": null,
92 | "_id": "5d0522979e94c50006de797b",
93 | "key": "5317",
94 | "name": "Come Back To Me (ft. Tove Lo) - Urban Cone (All Difficulties & Single Saber)",
95 | "uploader": {
96 | "_id": "5cff0b7298cc5a672c84ea67",
97 | "username": "skylerwallace"
98 | },
99 | "hash": "aaa14fb7dcaeda7a688db77617045a247baa151d",
100 | "uploaded": "2019-06-15T16:53:43.826Z",
101 | "converted": true,
102 | "downloadURL": "/api/download/key/5317",
103 | "coverURL": "/cdn/5317/aaa14fb7dcaeda7a688db77617045a247baa151d.jpg"
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/WebUtilities/HttpClientWrapper.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace WebUtilities
8 | {
9 | public class HttpClientWrapper : IWebClient
10 | {
11 | private HttpClient httpClient;
12 | public ILogger Logger;
13 |
14 | public HttpClientWrapper(HttpClient client = null)
15 | {
16 | if (client == null)
17 | httpClient = new HttpClient();
18 | else
19 | httpClient = client;
20 | ErrorHandling = ErrorHandling.ThrowOnException;
21 | }
22 |
23 | public int Timeout { get; set; }
24 | public ErrorHandling ErrorHandling { get; set; }
25 |
26 | public async Task GetAsync(Uri uri, bool completeOnHeaders, CancellationToken cancellationToken)
27 | {
28 | HttpCompletionOption completionOption =
29 | completeOnHeaders ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead;
30 | try
31 | {
32 | //TODO: Need testing for cancellation token
33 | return new HttpResponseWrapper(await httpClient.GetAsync(uri, completionOption, cancellationToken).ConfigureAwait(false));
34 | }
35 | catch(ArgumentException ex)
36 | {
37 | if (ErrorHandling != ErrorHandling.ReturnEmptyContent)
38 | throw;
39 | else
40 | {
41 | Logger?.Log(LogLevel.Error, $"Invalid URL, {uri?.ToString()}, passed to GetAsync()\n{ex.Message}\n{ex.StackTrace}");
42 | return new HttpResponseWrapper(null);
43 | }
44 | }
45 | catch(HttpRequestException ex)
46 | {
47 | if (ErrorHandling == ErrorHandling.ThrowOnException)
48 | throw;
49 | else
50 | {
51 | Logger?.Log(LogLevel.Error, $"Exception getting {uri?.ToString()}\n{ex.Message}\n{ex.StackTrace}");
52 | return new HttpResponseWrapper(null);
53 | }
54 | }
55 |
56 | }
57 |
58 | #region GetAsyncOverloads
59 |
60 | public Task GetAsync(string url, bool completeOnHeaders, CancellationToken cancellationToken)
61 | {
62 | var urlAsUri = string.IsNullOrEmpty(url) ? null : new Uri(url);
63 | return GetAsync(urlAsUri, completeOnHeaders, cancellationToken);
64 | }
65 | public Task GetAsync(string url)
66 | {
67 | return GetAsync(url, false, CancellationToken.None);
68 | }
69 | public Task GetAsync(string url, bool completeOnHeaders)
70 | {
71 | return GetAsync(url, completeOnHeaders, CancellationToken.None);
72 | }
73 | public Task GetAsync(string url, CancellationToken cancellationToken)
74 | {
75 | return GetAsync(url, false, cancellationToken);
76 | }
77 |
78 | public Task GetAsync(Uri uri)
79 | {
80 | return GetAsync(uri, false, CancellationToken.None);
81 | }
82 | public Task GetAsync(Uri uri, bool completeOnHeaders)
83 | {
84 | return GetAsync(uri, completeOnHeaders, CancellationToken.None);
85 | }
86 | public Task GetAsync(Uri uri, CancellationToken cancellationToken)
87 | {
88 | return GetAsync(uri, false, cancellationToken);
89 | }
90 | #endregion
91 |
92 | #region IDisposable Support
93 | private bool disposedValue = false; // To detect redundant calls
94 |
95 | protected virtual void Dispose(bool disposing)
96 | {
97 | if (!disposedValue)
98 | {
99 | if (disposing)
100 | {
101 | if (httpClient != null)
102 | {
103 | httpClient.Dispose();
104 | httpClient = null;
105 | }
106 | }
107 | disposedValue = true;
108 | }
109 | }
110 |
111 | public void Dispose()
112 | {
113 | Dispose(true);
114 | }
115 | #endregion
116 |
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/WebUtilities/HttpContentWrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Net;
7 | using System.Net.Http;
8 | using System.Collections.ObjectModel;
9 |
10 | namespace WebUtilities
11 | {
12 | public class HttpContentWrapper : IWebResponseContent
13 | {
14 | private HttpContent _content;
15 | public HttpContentWrapper(HttpContent content)
16 | {
17 | _content = content;
18 | _headers = new Dictionary>();
19 | if (_content?.Headers != null)
20 | {
21 | foreach (var header in _content.Headers)
22 | {
23 | _headers.Add(header.Key, header.Value);
24 | }
25 | }
26 | }
27 |
28 | protected Dictionary> _headers;
29 | public ReadOnlyDictionary> Headers
30 | {
31 | get { return new ReadOnlyDictionary>(_headers); }
32 | }
33 |
34 | public string ContentType { get { return _content?.Headers?.ContentType?.MediaType; } }
35 |
36 | public Task ReadAsByteArrayAsync()
37 | {
38 | return _content?.ReadAsByteArrayAsync();
39 | }
40 |
41 | public Task ReadAsStreamAsync()
42 | {
43 | return _content?.ReadAsStreamAsync();
44 | }
45 |
46 | public Task ReadAsStringAsync()
47 | {
48 | return _content?.ReadAsStringAsync();
49 | }
50 |
51 | public Task ReadAsFileAsync(string filePath, bool overwrite)
52 | {
53 | if (_content == null)
54 | return null;
55 | string pathname = Path.GetFullPath(filePath);
56 | if (!overwrite && File.Exists(filePath))
57 | {
58 | throw new InvalidOperationException(string.Format("File {0} already exists.", pathname));
59 | }
60 |
61 | FileStream fileStream = null;
62 | try
63 | {
64 | fileStream = new FileStream(pathname, FileMode.Create, FileAccess.Write, FileShare.None);
65 | return _content.CopyToAsync(fileStream).ContinueWith(
66 | (copyTask) =>
67 | {
68 | fileStream.Close();
69 | });
70 | }
71 | catch
72 | {
73 | if (fileStream != null)
74 | {
75 | fileStream.Close();
76 | }
77 |
78 | throw;
79 | }
80 | }
81 |
82 | #region IDisposable Support
83 | private bool disposedValue = false; // To detect redundant calls
84 |
85 | protected virtual void Dispose(bool disposing)
86 | {
87 | if (!disposedValue)
88 | {
89 | if (disposing)
90 | {
91 | if (_content != null)
92 | {
93 | _content.Dispose();
94 | _content = null;
95 | }
96 | }
97 | disposedValue = true;
98 | }
99 | }
100 |
101 | public void Dispose()
102 | {
103 | Dispose(true);
104 | }
105 | #endregion
106 |
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/WebUtilities/HttpResponseWrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Text;
7 |
8 | namespace WebUtilities
9 | {
10 | public class HttpResponseWrapper : IWebResponseMessage
11 | {
12 | private HttpResponseMessage _response;
13 | public HttpStatusCode StatusCode { get { return _response.StatusCode; } }
14 |
15 | public bool IsSuccessStatusCode { get { return _response.IsSuccessStatusCode; } }
16 |
17 | public IWebResponseContent Content { get; protected set; }
18 |
19 | private Dictionary> _headers;
20 | public ReadOnlyDictionary> Headers
21 | {
22 | get { return new ReadOnlyDictionary>(_headers); }
23 | }
24 |
25 | public string ReasonPhrase { get { return _response.ReasonPhrase; } }
26 |
27 | public HttpResponseWrapper(HttpResponseMessage response)
28 | {
29 | _response = response;
30 | Content = new HttpContentWrapper(response?.Content);
31 | _headers = new Dictionary>();
32 | if (_response?.Headers != null)
33 | {
34 | foreach (var header in _response.Headers)
35 | {
36 | _headers.Add(header.Key, header.Value);
37 | }
38 | }
39 | }
40 |
41 |
42 | #region IDisposable Support
43 | private bool disposedValue = false; // To detect redundant calls
44 |
45 | protected virtual void Dispose(bool disposing)
46 | {
47 | if (!disposedValue)
48 | {
49 | if (disposing)
50 | {
51 | if (Content != null)
52 | {
53 | Content.Dispose();
54 | Content = null;
55 | }
56 | }
57 | disposedValue = true;
58 | }
59 | }
60 |
61 | public void Dispose()
62 | {
63 | Dispose(true);
64 | }
65 | #endregion
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/WebUtilities/IWebClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace WebUtilities
8 | {
9 | public interface IWebClient : IDisposable
10 | {
11 | int Timeout { get; set; }
12 | ErrorHandling ErrorHandling { get; set; }
13 | Task GetAsync(Uri uri);
14 | Task GetAsync(Uri uri, bool completeOnHeaders);
15 | Task GetAsync(Uri uri, CancellationToken cancellationToken);
16 | Task GetAsync(Uri uri, bool completeOnHeaders, CancellationToken cancellationToken);
17 | Task GetAsync(string url);
18 | Task GetAsync(string url, bool completeOnHeaders);
19 | Task GetAsync(string url, CancellationToken cancellationToken);
20 | Task GetAsync(string url, bool completeOnHeaders, CancellationToken cancellationToken);
21 |
22 | }
23 |
24 | public enum ErrorHandling
25 | {
26 | ThrowOnException,
27 | ThrowOnWebFault,
28 | ReturnEmptyContent
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/WebUtilities/IWebResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Net;
5 | using System.Threading.Tasks;
6 | using System.Threading;
7 | using System.Net.Http;
8 | using System.IO;
9 | using System.Collections.ObjectModel;
10 |
11 | namespace WebUtilities
12 | {
13 | public interface IWebResponseMessage : IDisposable
14 | {
15 | HttpStatusCode StatusCode { get; }
16 | string ReasonPhrase { get; }
17 | bool IsSuccessStatusCode { get; }
18 | IWebResponseContent Content { get; }
19 |
20 | ReadOnlyDictionary> Headers { get; }
21 | }
22 |
23 | public interface IWebResponseContent : IDisposable
24 | {
25 |
26 | Task ReadAsStringAsync();
27 | Task ReadAsStreamAsync();
28 | Task ReadAsByteArrayAsync();
29 | Task ReadAsFileAsync(string filePath, bool overwrite);
30 |
31 | string ContentType { get; }
32 | ReadOnlyDictionary> Headers { get; }
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/WebUtilities/WebUtilities.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/update_submodules.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | git submodule foreach git pull origin master
3 | echo Finshed...
4 | pause
--------------------------------------------------------------------------------