diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js new file mode 100644 index 0000000..072996a --- /dev/null +++ b/classes/CSteamSharedfile.js @@ -0,0 +1,152 @@ +const Cheerio = require("cheerio"); +const SteamID = require("steamid"); +const SteamCommunity = require("../index.js"); +const steamIdResolver = require("steamid-resolver"); +const ESharedfileType = require("../resources/ESharedfileType.js"); + + +/** + * Scrape a sharedfile's DOM to get all available information + * @param {String} sid - ID of the sharedfile + * @param {function} callback - First argument is null/Error, second is object containing all available information + */ +SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { + // Construct object holding all the data we can scrape + let sharedfile = { + id: sid, + type: null, + appID: null, + owner: null, + fileSize: null, + postDate: null, + resolution: null, + uniqueVisitorsCount: null, + favoritesCount: null, + upvoteCount: null + } + + + // Get DOM of sharedfile + this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sid}`, (err, res, body) => { + try { + + /* --------------------- Preprocess output --------------------- */ + + // Load output into cheerio to make parsing easier + let $ = Cheerio.load(body); + + // 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(); + let detailsRight = $(".detailsStatsContainerRight").children(); + + Object.keys(detailsLeft).forEach((e) => { // Dynamically get all details. Don't hardcore so that this also works for guides. + if (isNaN(e)) return; // Ignore invalid entries + + 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(); + + Object.keys(statsTable).forEach((e, i) => { + if (isNaN(e)) return; // Ignore invalid entries + + // Value description is at index 3, value data at index 1 + statsTableObj[statsTable[e].children[3].children[0].data] = statsTable[e].children[1].children[0].data.replace(/,/g, ""); // Remove commas from 1k+ values + }); + + + /* --------------------- Find and map values --------------------- */ + + // Find appID in share button onclick event + sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sid}', '`, "").replace("' );", "")) + + + // Find owner profile link, convert to steamID64 using steamIdResolver lib and create a SteamID object + let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; + + steamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // Note: Callback will be called before this is done, takes around 1 sec to populate + if (!err) sharedfile.owner = new SteamID(steamID64); + }); + + + // Find fileSize if not guide + sharedfile.fileSize = detailsStatsObj["File Size"] || null; // TODO: Convert to bytes? It seems like to always be MB but no guarantee + + // Find postDate and convert to timestamp (Warning: Will get ugly) + let posted = detailsStatsObj["Posted"].replace(/,|@/g, "").split(" "); // Remove comma behind month and @, the date & time separator. Split by space to get: Day, Month, Year (if not current) and time + let months = { "Jan": "01", "Feb": "02", "Mar": "03", "Apr": "04", "May": "05", "Jun": "06", "Jul": "07", "Aug": "08", "Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12" }; // Map all month abbreviations + + if (posted[0].split(":")[0].length == 1) posted[0] = "0" + posted[0]; // Add zero if day is <10 to have a fixed length + + posted[1] = months[posted[1]]; // Replace month abbreviation with corresponding Number + + if (!posted[2]) posted[2] = new Date().getUTCFullYear().toString(); // Add current year if Steam did not list one + else posted.splice(3, 1); // ...otherwise remove element 3 as it will be an empty String + + // Convert AM/PM time to 24h format - Credit: https://stackoverflow.com/a/40197728 (Modified) + if (posted[3].split(":")[0].length == 1) posted[3] = "0" + posted[3]; // Add zero if hour is <10 to have a fixed length + + let time = posted[3].substring(0, 5); + let modifier = posted[3].substring(5, 7); + let [hours, minutes] = time.split(":"); + + if (hours === "12") hours = "00"; + if (modifier === "pm") hours = parseInt(hours, 10) + 12; + + sharedfile.postDate = Date.parse(`${posted[2]}-${posted[1]}-${posted[0]}T${hours}:${minutes}:00.000Z`); // Construct Date String and parse it to get Unix timestamp + + + // Find resolution if artwork or screenshot + sharedfile.resolution = detailsStatsObj["Size"] || null; + + + // Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false + if (statsTableObj["Unique Visitors"]) sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]); + + + // Find favoritesCount. We can't use ' || null' here as Number("0") casts to false + if (statsTableObj["Current Favorites"]) sharedfile.favoritesCount = Number(statsTableObj["Current Favorites"]); + + + // Find upvoteCount. We can't use ' || null' here as Number("0") casts to false + let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text(); + if (upvoteCount) sharedfile.upvoteCount = Number(upvoteCount); + + + // 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; + + + callback(null, new CSteamSharedfile(this, sharedfile)); + + } catch (err) { + callback(err, null); + } + }); +} + + +function CSteamSharedfile(community, data) { + this._community = community; + + // Clone all the data we recieved + Object.assign(this, data); // TODO: This is cleaner but might break IntelliSense + + /* this.id = data.id; + this.type = data.type; + this.appID = data.appID; + this.owner = data.owner; + this.fileSize = data.fileSize; + this.postDate = data.postDate; + this.resolution = data.resolution; + this.uniqueVisitorsCount = data.uniqueVisitorsCount; + this.favoritesCount = data.favoritesCount; + this.upvoteCount = data.upvoteCount; */ +} \ No newline at end of file diff --git a/package.json b/package.json index afd186a..64709eb 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "request": "^2.88.0", "steam-totp": "^1.5.0", "steamid": "^1.1.3", + "steamid-resolver": "^1.2.3", "xml2js": "^0.4.22" }, "engines": {