From f516e05f3fd2942753e46078cd93359cded94376 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 5 May 2024 13:45:30 +0200 Subject: [PATCH 1/6] feat: Add support for detecting workshop item sharedfiles --- classes/CSteamSharedFile.js | 48 ++++++++++++++++++++++-------------- resources/ESharedFileType.js | 4 ++- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index 277dbb9..ca84782 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -39,6 +39,27 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { // Load output into cheerio to make parsing easier let $ = Cheerio.load(body); + + // Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest + let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || ""; + + if (breadcrumb.includes("Screenshot")) { + sharedfile.type = ESharedFileType.Screenshot; + } + + if (breadcrumb.includes("Artwork")) { + sharedfile.type = ESharedFileType.Artwork; + } + + if (breadcrumb.includes("Guide")) { + sharedfile.type = ESharedFileType.Guide; + } + + if (breadcrumb.includes("Workshop")) { + sharedfile.type = ESharedFileType.Workshop; + } + + // Dynamically map detailsStatsContainerLeft to detailsStatsContainerRight in an object to make readout easier. It holds size, post date and resolution. let detailsStatsObj = {}; let detailsLeft = $(".detailsStatsContainerLeft").children(); @@ -52,6 +73,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { detailsStatsObj[detailsLeft[e].children[0].data.trim()] = detailsRight[e].children[0].data; }); + // Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites let statsTableObj = {}; let statsTable = $(".stats_table").children(); @@ -82,8 +104,14 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { sharedfile.postDate = Helpers.decodeSteamTime(posted); - // Find resolution if artwork or screenshot - sharedfile.resolution = detailsStatsObj["Size"] || null; + // Find resolution if artwork or screenshot. Guides don't have a resolution and workshop items display it somewhere else + if (sharedfile.type != ESharedFileType.Workshop) { + sharedfile.resolution = detailsStatsObj["Size"] || null; + } else { + let resolutionTag = $(".workshopTagsTitle:contains(\"Resolution:\")").next(); + + sharedfile.resolution = resolutionTag.text() || null; // Keep prop null if this workshop item does not have a resolution + } // Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false @@ -117,22 +145,6 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled" - // Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest - let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || ""; - - if (breadcrumb.includes("Screenshot")) { - sharedfile.type = ESharedFileType.Screenshot; - } - - if (breadcrumb.includes("Artwork")) { - sharedfile.type = ESharedFileType.Artwork; - } - - if (breadcrumb.includes("Guide")) { - sharedfile.type = ESharedFileType.Guide; - } - - // Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; diff --git a/resources/ESharedFileType.js b/resources/ESharedFileType.js index 21290c8..01f006a 100644 --- a/resources/ESharedFileType.js +++ b/resources/ESharedFileType.js @@ -5,9 +5,11 @@ module.exports = { "Screenshot": 0, "Artwork": 1, "Guide": 2, + "Workshop": 3, // Value-to-name mapping for convenience "0": "Screenshot", "1": "Artwork", - "2": "Guide" + "2": "Guide", + "3": "Workshop" }; From 8d0489c89833880e760de3cacfbf70618e381684 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Tue, 7 May 2024 17:23:16 +0200 Subject: [PATCH 2/6] feat: Scrape categories from guides and workshop items --- classes/CSteamSharedFile.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index ca84782..b7c9cae 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -22,6 +22,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { fileSize: null, postDate: null, resolution: null, + categories: [], uniqueVisitorsCount: null, favoritesCount: null, upvoteCount: null, @@ -114,6 +115,14 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { } + // Find categories if guide or workshop item + if (sharedfile.type == ESharedFileType.Guide || sharedfile.type == ESharedFileType.Workshop) { + let categoryTag = $(".workshopTagsTitle:contains(\"Category:\")").parent().contents().slice(1).text(); // Find div containing 'Category:' workshopTagsTitle, remove first element 'Category:' and get everything else as text + + sharedfile.categories = categoryTag ? categoryTag.split(", ") : []; // Convert to array if string is not empty (aka no categories have been found) + } + + // Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false if (statsTableObj["Unique Visitors"]) { sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]); @@ -170,7 +179,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { * Constructor - Creates a new SharedFile object * @class * @param {SteamCommunity} community - * @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data + * @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, category: string[], uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data */ function CSteamSharedFile(community, data) { /** From 7675de349735fa9b54fa083444da5749d57c963f Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Tue, 7 May 2024 19:31:54 +0200 Subject: [PATCH 3/6] feat: Scrape tags from workshop items --- classes/CSteamSharedFile.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index b7c9cae..7ecf4c9 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -23,6 +23,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { postDate: null, resolution: null, categories: [], + tags: [], uniqueVisitorsCount: null, favoritesCount: null, upvoteCount: null, @@ -123,6 +124,12 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { } + // Find tags (there can be multiple) + let tagsTag = $(".workshopTagsTitle:contains(\"Tags:\")").next().contents(); + + sharedfile.tags = tagsTag.map((i, e) => e.type === 'text' ? $(e).text() : '').get() || []; // Map text to an array - https://stackoverflow.com/a/31543727 + + // Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false if (statsTableObj["Unique Visitors"]) { sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]); @@ -179,7 +186,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { * Constructor - Creates a new SharedFile object * @class * @param {SteamCommunity} community - * @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, category: string[], uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data + * @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, category: string[], tags: string[], uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data */ function CSteamSharedFile(community, data) { /** From a4efe4a7747bfbe0909af66207b668ab6a0168d2 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Tue, 7 May 2024 23:06:50 +0200 Subject: [PATCH 4/6] feat: Implement functions to un-/subscribe from/to workshop item sharedfiles --- classes/CSteamSharedFile.js | 16 +++++++++++++ components/sharedfiles.js | 48 ++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index 7ecf4c9..f3130bc 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -247,3 +247,19 @@ CSteamSharedFile.prototype.unfavorite = function(callback) { CSteamSharedFile.prototype.unsubscribe = function(callback) { this._community.unsubscribeSharedFileComments(this.owner, this.id, callback); }; + +/** + * Subscribes to this workshop item + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamSharedFile.prototype.subscribeWorkshop = function(callback) { + this._community.subscribeWorkshopSharedFile(this.id, this.appID, callback); +}; + +/** + * Unsubscribes from this workshop item + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamSharedFile.prototype.unsubscribeWorkshop = function(callback) { + this._community.unsubscribeWorkshopSharedFile(this.id, this.appID, callback); +}; diff --git a/components/sharedfiles.js b/components/sharedfiles.js index b4fa2c7..60fb633 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -132,7 +132,7 @@ SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, ca }; /** - * Unsubscribes from a sharedfile's comment section. Note: Checkbox on webpage does not update + * Unsubscribes from a sharedfile's comment section. * @param {SteamID | String} userID - ID of the user associated to this sharedfile * @param {String} sharedFileId - ID of the sharedfile * @param {function} callback - Takes only an Error object/null as the first argument @@ -156,3 +156,49 @@ SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, shared callback(err); }, "steamcommunity"); }; + +/** + * Subscribes to a workshop item sharedfile. + * @param {String} sharedFileId - ID of the sharedfile + * @param {String} appid - ID of the app associated to this sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.subscribeWorkshopSharedFile = function(sharedFileId, appid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/subscribe", + "form": { + "id": sharedFileId, + "appid": appid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); +}; + +/** + * Unsubscribes from a workshop item sharedfile. + * @param {String} sharedFileId - ID of the sharedfile + * @param {String} appid - ID of the app associated to this sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.unsubscribeWorkshopSharedFile = function(sharedFileId, appid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/unsubscribe", + "form": { + "id": sharedFileId, + "appid": appid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); +}; From c36efc6fc86eacadd64a7d65947400e3ffdc3080 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Tue, 7 May 2024 23:07:26 +0200 Subject: [PATCH 5/6] refactor: Rename comment subscribing functions to separate them more clearly from workshop item subscribe functions --- classes/CSteamSharedFile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index f3130bc..0029f8b 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -228,7 +228,7 @@ CSteamSharedFile.prototype.comment = function(message, callback) { * Subscribes to this sharedfile's comment section. Note: Checkbox on webpage does not update * @param {function} callback - Takes only an Error object/null as the first argument */ -CSteamSharedFile.prototype.subscribe = function(callback) { +CSteamSharedFile.prototype.subscribeComments = function(callback) { this._community.subscribeSharedFileComments(this.owner, this.id, callback); }; @@ -244,7 +244,7 @@ CSteamSharedFile.prototype.unfavorite = function(callback) { * Unsubscribes from this sharedfile's comment section. Note: Checkbox on webpage does not update * @param {function} callback - Takes only an Error object/null as the first argument */ -CSteamSharedFile.prototype.unsubscribe = function(callback) { +CSteamSharedFile.prototype.unsubscribeComments = function(callback) { this._community.unsubscribeSharedFileComments(this.owner, this.id, callback); }; From a674737cd9961d01c358edf5113e40ce9df5668b Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 15 Feb 2025 18:35:51 +0100 Subject: [PATCH 6/6] fix: Fix getSteamSharedFile() failing to detect vote status of greenlight items --- classes/CSteamSharedFile.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index 0029f8b..6ef29cb 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -157,8 +157,11 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { // Determine if this account has already voted on this sharedfile - sharedfile.isUpvoted = String($(".workshopItemControlCtn > #VoteUpBtn")[0].attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled" - sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled" + const voteUpBtn = $(".workshopItemControlCtn > #VoteUpBtn")[0] || $(".greenlight_controls > #VoteUpBtn")[0]; // workshopItemControlCtn for "normal" items, greenlight_controls for items which can be voted into a game (e.g. CS skins) + const voteDownBtn = $(".workshopItemControlCtn > #VoteDownBtn")[0] || $(".greenlight_controls > #VoteDownBtn")[0]; + + sharedfile.isUpvoted = String(voteUpBtn.attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled" + sharedfile.isDownvoted = String(voteDownBtn.attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled" // Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object