2023-05-15 00:52:16 +08:00
const Cheerio = require ( 'cheerio' ) ;
const SteamID = require ( 'steamid' ) ;
2023-06-24 14:39:16 +08:00
2023-05-15 00:52:16 +08:00
const SteamCommunity = require ( '../index.js' ) ;
2023-06-24 14:39:16 +08:00
const Helpers = require ( '../components/helpers.js' ) ;
const ESharedFileType = require ( '../resources/ESharedFileType.js' ) ;
2023-05-14 21:00:45 +08:00
/ * *
* Scrape a sharedfile ' s DOM to get all available information
2023-06-24 14:39:16 +08:00
* @ param { string } sharedFileId - ID of the sharedfile
2023-05-16 04:35:39 +08:00
* @ param { function } callback - First argument is null / Error , second is object containing all available information
2023-05-14 21:00:45 +08:00
* /
2023-06-24 14:39:16 +08:00
SteamCommunity . prototype . getSteamSharedFile = function ( sharedFileId , callback ) {
2023-05-15 00:52:16 +08:00
// Construct object holding all the data we can scrape
let sharedfile = {
2023-05-16 04:45:33 +08:00
id : sharedFileId ,
2023-05-15 00:52:16 +08:00
type : null ,
appID : null ,
owner : null ,
fileSize : null ,
postDate : null ,
resolution : null ,
uniqueVisitorsCount : null ,
favoritesCount : null ,
2023-05-28 19:45:00 +08:00
upvoteCount : null ,
2023-05-28 20:02:40 +08:00
guideNumRatings : null ,
2023-05-28 19:45:00 +08:00
isUpvoted : null ,
isDownvoted : null
2023-05-15 00:52:16 +08:00
} ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Get DOM of sharedfile
2023-05-16 04:45:33 +08:00
this . httpRequestGet ( ` https://steamcommunity.com/sharedfiles/filedetails/?id= ${ sharedFileId } ` , ( err , res , body ) => {
2023-05-15 00:52:16 +08:00
try {
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
/* --------------------- Preprocess output --------------------- */
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Load output into cheerio to make parsing easier
let $ = Cheerio . load ( body ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// 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 ( ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
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
}
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
detailsStatsObj [ detailsLeft [ e ] . children [ 0 ] . data . trim ( ) ] = detailsRight [ e ] . children [ 0 ] . data ;
} ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites
let statsTableObj = { } ;
let statsTable = $ ( ".stats_table" ) . children ( ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
Object . keys ( statsTable ) . forEach ( ( e ) => {
if ( isNaN ( e ) ) {
return ; // Ignore invalid entries
}
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// 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
} ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
/* --------------------- Find and map values --------------------- */
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find appID in share button onclick event
2023-05-16 04:45:33 +08:00
sharedfile . appID = Number ( $ ( "#ShareItemBtn" ) . attr ( ) [ "onclick" ] . replace ( ` ShowSharePublishedFilePopup( ' ${ sharedFileId } ', ' ` , "" ) . replace ( "' );" , "" ) ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// 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
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find postDate and convert to timestamp
let posted = detailsStatsObj [ "Posted" ] . trim ( ) ;
2023-05-14 21:00:45 +08:00
2023-06-24 14:48:26 +08:00
sharedfile . postDate = Helpers . decodeSteamTime ( posted ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find resolution if artwork or screenshot
sharedfile . resolution = detailsStatsObj [ "Size" ] || null ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false
if ( statsTableObj [ "Unique Visitors" ] ) {
sharedfile . uniqueVisitorsCount = Number ( statsTableObj [ "Unique Visitors" ] ) ;
}
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find favoritesCount. We can't use ' || null' here as Number("0") casts to false
if ( statsTableObj [ "Current Favorites" ] ) {
sharedfile . favoritesCount = Number ( statsTableObj [ "Current Favorites" ] ) ;
}
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
// Find upvoteCount. We can't use ' || null' here as Number("0") casts to false
let upvoteCount = $ ( "#VotesUpCountContainer > #VotesUpCount" ) . text ( ) ;
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
if ( upvoteCount ) {
sharedfile . upvoteCount = Number ( upvoteCount ) ;
}
2023-05-14 21:00:45 +08:00
2023-05-28 20:02:40 +08:00
// Find numRatings if this is a guide as they use a different voting system
2023-05-30 01:29:24 +08:00
let numRatings = $ ( ".ratingSection > .numRatings" ) . text ( ) . replace ( " ratings" , "" ) ;
2023-05-28 20:02:40 +08:00
sharedfile . guideNumRatings = Number ( numRatings ) || null ; // Set to null if not a guide or if the guide does not have enough ratings to show a value
2023-05-28 19:45:00 +08:00
// 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"
2023-05-15 00:52:16 +08:00
// 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 || "" ;
2023-05-14 22:12:59 +08:00
2023-05-15 00:52:16 +08:00
if ( breadcrumb . includes ( "Screenshot" ) ) {
2023-06-24 14:39:16 +08:00
sharedfile . type = ESharedFileType . Screenshot ;
2023-05-15 00:52:16 +08:00
}
2023-05-14 22:12:59 +08:00
2023-05-15 00:52:16 +08:00
if ( breadcrumb . includes ( "Artwork" ) ) {
2023-06-24 14:39:16 +08:00
sharedfile . type = ESharedFileType . Artwork ;
2023-05-15 00:52:16 +08:00
}
2023-05-14 21:00:45 +08:00
2023-05-15 00:52:16 +08:00
if ( breadcrumb . includes ( "Guide" ) ) {
2023-06-24 14:39:16 +08:00
sharedfile . type = ESharedFileType . Guide ;
2023-05-15 00:52:16 +08:00
}
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
let ownerHref = $ ( ".friendBlockLinkOverlay" ) . attr ( ) [ "href" ] ;
2023-05-16 04:35:39 +08:00
Helpers . resolveVanityURL ( ownerHref , ( err , data ) => { // This request takes <1 sec
2023-06-24 14:43:09 +08:00
if ( err ) {
callback ( err ) ;
return ;
2023-05-15 00:52:16 +08:00
}
2023-06-24 14:43:09 +08:00
sharedfile . owner = new SteamID ( data . steamID ) ;
2023-05-15 00:52:16 +08:00
// Make callback when ID was resolved as otherwise owner will always be null
2023-06-24 14:39:16 +08:00
callback ( null , new CSteamSharedFile ( this , sharedfile ) ) ;
2023-05-15 00:52:16 +08:00
} ) ;
} catch ( err ) {
callback ( err , null ) ;
}
} , "steamcommunity" ) ;
2023-05-14 21:22:15 +08:00
} ;
2023-05-14 21:00:45 +08:00
2023-05-30 01:29:24 +08:00
/ * *
2023-06-24 14:39:16 +08:00
* Constructor - Creates a new SharedFile object
2023-05-30 01:29:24 +08:00
* @ class
* @ param { SteamCommunity } community
2023-06-24 14:39:16 +08:00
* @ 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
2023-05-30 01:29:24 +08:00
* /
2023-06-24 14:39:16 +08:00
function CSteamSharedFile ( community , data ) {
2023-05-30 01:29:24 +08:00
/ * *
* @ type { SteamCommunity }
* /
2023-05-15 00:52:16 +08:00
this . _community = community ;
2023-06-24 14:39:16 +08:00
// Clone all the data we received
2023-05-30 01:29:24 +08:00
Object . assign ( this , data ) ;
2023-05-14 21:26:55 +08:00
}
/ * *
* Deletes a comment from this sharedfile ' s comment section
* @ param { String } cid - ID of the comment to delete
* @ param { function } callback - Takes only an Error object / null as the first argument
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . deleteComment = function ( cid , callback ) {
this . _community . deleteSharedFileComment ( this . userID , this . id , cid , callback ) ;
2023-05-14 21:26:55 +08:00
} ;
/ * *
* Favorites this sharedfile
* @ param { function } callback - Takes only an Error object / null as the first argument
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . favorite = function ( callback ) {
this . _community . favoriteSharedFile ( this . id , this . appID , callback ) ;
2023-05-14 21:26:55 +08:00
} ;
/ * *
* Posts a comment to this sharedfile
* @ param { String } message - Content of the comment to post
* @ param { function } callback - Takes only an Error object / null as the first argument
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . comment = function ( message , callback ) {
this . _community . postSharedFileComment ( this . owner , this . id , message , callback ) ;
2023-05-14 21:26:55 +08:00
} ;
/ * *
* 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
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . subscribe = function ( callback ) {
this . _community . subscribeSharedFileComments ( this . owner , this . id , callback ) ;
2023-05-14 21:26:55 +08:00
} ;
/ * *
* Unfavorites this sharedfile
* @ param { function } callback - Takes only an Error object / null as the first argument
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . unfavorite = function ( callback ) {
this . _community . unfavoriteSharedFile ( this . id , this . appID , callback ) ;
2023-05-14 21:26:55 +08:00
} ;
/ * *
* 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
* /
2023-06-24 14:39:16 +08:00
CSteamSharedFile . prototype . unsubscribe = function ( callback ) {
this . _community . unsubscribeSharedFileComments ( this . owner , this . id , callback ) ;
2023-05-14 21:26:55 +08:00
} ;