achievements-app / psn-api

A JavaScript library that lets you get trophy, user, and game data from the PlayStation Network.
https://psn-api.achievements.app
MIT License
263 stars 31 forks source link

Relate trophy titles and user titles #162

Open zuinqstudio opened 4 months ago

zuinqstudio commented 4 months ago

I'm trying to gather some basic player statistics from gamelist/v2/users/$accountId/titles and trophy/v1/users/$accountId/trophyTitles. Unfortunately, conceptId appears only in the first request, and the best field to find a game by its name from the second one trophyTitleName, which slightly differs from the original game title in lots of cases.

Is there any known api to establish this relationship?

zuinqstudio commented 4 months ago

Answering my own question, just in case might be useful for someone. I found this.-

https://m.np.playstation.com/api/trophy/v1/users/{userId}|me/titles/trophyTitles?npTitleIds={ids}

Which returns objects that include npTitleId and npCommunicationId. This npCommunicationId can later be used in trophyGroups methods.

platinumachievements commented 3 months ago

Answering my own question, just in case might be useful for someone. I found this.-

https://m.np.playstation.com/api/trophy/v1/users/{userId}|me/titles/trophyTitles?npTitleIds={ids}

Which returns objects that include npTitleId and npCommunicationId. This npCommunicationId can later be used in trophyGroups methods.

what is the best way of getting the titleids for this, i cant seem to find a good way

pacMakaveli commented 1 month ago

Hey, if you're looking for something like this, hit me up. games.directory does a lot of mapping between titles and games. Let me know your use-case and I can provide an API endpoint.

pradella commented 1 month ago

Hey @pacMakaveli, I tried to sign up on the games directory, but it seems to ben't working atm. Do you have any tips on how to get the titleId from npCommunicationId?

I am currently struggling to get media and playDuration for PS3 games.

pacMakaveli commented 1 month ago

@pradella sorry about that. If you want to, please let me know what didn't work so I can fix it :D

As for the npCommunicationId to titleId, it really depends on the use case and what data you've got available. This is a ruby script that I use:

require 'json'

class String
  def levenshtein_distance(other)
    m, n = self.length, other.length
    return m if n == 0
    return n if m == 0
    d = Array.new(m+1) {Array.new(n+1)}

    (0..m).each {|i| d[i][0] = i}
    (0..n).each {|j| d[0][j] = j}
    (1..n).each do |j|
      (1..m).each do |i|
        d[i][j] = if self[i-1] == other[j-1]
                    d[i-1][j-1]
                  else
                    [d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+1].min
                  end
      end
    end
    d[m][n]
  end
end

def parse_list(content)
  content.split("\n").map do |line|
    id, name = line.split(': ', 2)
    [id.strip, name.strip]
  end
end

def normalize_name(name)
  name.downcase.gsub(/[^a-z0-9]+/, '')
end

def calculate_accuracy(distance, max_length)
  1 - (distance.to_f / max_length)
end

def find_best_match(entry, list)
  normalized_entry = normalize_name(entry[1])
  best_match = list.max_by do |item|
    normalized_item = normalize_name(item[1])
    accuracy = calculate_accuracy(
      normalized_entry.levenshtein_distance(normalized_item),
      [normalized_entry.length, normalized_item.length].max
    )
    accuracy
  end

  accuracy = calculate_accuracy(
    normalized_entry.levenshtein_distance(normalize_name(best_match[1])),
    [normalized_entry.length, normalize_name(best_match[1]).length].max
  )

  [best_match, accuracy]
end

def check(entry, list)
  best_match, accuracy = find_best_match(entry, list)

  {
    entry: entry[0],
    match: best_match[0],
    accuracy: accuracy,
    labels: [entry[1], best_match[1]]
  }
end

def cross_reference(list1, list2)
  result = {}

  list1.each do |entry|
    match = check(entry, list2)

    # You may want to adjust this threshold
    if match[:accuracy] >= 0.7
      key = normalize_name(entry[1])
      result[key] ||= []
      result[key] << match
    end
  end

  result.map { |k, v| { k => v } }
end

# Function to read file content
def read_file(filename)
  File.read(filename)
rescue Errno::ENOENT
  puts "File #{filename} not found."
  exit
end

# Main execution
if ARGV.length == 2
  # Cross-reference two lists
  games_content = read_file(ARGV[0])
  titles_content = read_file(ARGV[1])

  games = parse_list(games_content)
  titles = parse_list(titles_content)

  result = cross_reference(games, titles)
elsif ARGV.length == 3 && ARGV[0] == 'check'
  # Check a single entry against a list
  entry_content = read_file(ARGV[1])
  list_content = read_file(ARGV[2])

  entry = parse_list(entry_content).first
  list = parse_list(list_content)

  result = check(entry, list)
else
  puts "Usage: ruby script.rb [games_file] [titles_file]"
  puts "   or: ruby script.rb check [entry_file] [list_file]"
  exit
end

# Output the result as JSON
puts JSON.pretty_generate(result)

And then these are the files I would give it.

games =>

PPSA01923_00: Fortnite
CUSA01433_00: Rocket League®
CUSA15495_00: Surviving the Aftermath
PPSA01463_00: Borderlands 3
CUSA08025_00: Borderlands 3

titles =>

NPWR21035_00: Borderlands® 3
NPWR28289_00: XDefiant
NPWR28312_00: Crime Boss: Rockay City
NPWR29727_00: The Outer Worlds: Spacer's Choice Edition

The gist of it is that it tried to match them by name. If no match is found, it will use the levenshtein algorithm to find the closes match, otherwise it will return nothing.

playDuration is not available for PS3 games. This data was exposed when PS5 was released and AFAIK, there was no tracking of this kind for PS3 titles. media, what sort of media are you looking for?

https://splash.games.directory/ https://splash.games.directory/i?q%5Bslug_cont_any%5D=fortnite

has probably more than what you need. It doesn't currently allow you to retrieve via some sort of API, but it's something I can add quickly. If you want to do it yourself though, you will need to query the concept API. I don't think this repo has any documentation (not really looked into it tbh). These are the endpoints I personally use in my projects. They use the same authentication method as the app.

https://m.np.playstation.net/api/catalog/v2/concepts/fetch?country=GB&language=en&age=999&limit=1000&offset=1 https://m.np.playstation.net/api/catalog/v2/concepts/10000470 https://m.np.playstation.net/api/catalog/v2/titles/CUSA18182_00/?country=GB&language=en&age=999

pradella commented 1 month ago

Wow, thanks for sharing those additional endpoints @pacMakaveli !

You're right; PS3 playDuration does not exist (I completely forgot it).

Indeed I could find npCommunicationId from titleId; but not the opposite (titleId from npCommunicationId).

For example: trophy/v1/users/${accountId}/trophyTitles brings all games (including PS3, but only npCommunicationId, no titleId. And to fetch media Im using this one gamelist/v2/users/${accountId}/titles/${titleId}

I'm guessing you have previously mapped all those npCommunicationId with titleId based on the history scan your app made?

pacMakaveli commented 1 month ago

@pradella correct. My database spans 9 years of constantly syncing the API and more importantly, users. One easy way to map these is to scan someone that you know has completed/owns lots of games. Hakoom, Ikemenzi etc.. (https://psnprofiles.com/dav1d_123) you can get their account ids from the search endpoint.

Again, if there's a way I can provide this data to you via games.directory's API, please let me know. I'd be more than happy as one of my goals with this is to provide little snippets of APIs for these kind of use cases. (obviously free, just in case it's not clear)

platinumachievements commented 1 month ago

@pacMakaveli Hi! Sorry to quickly hijack this thread, I would love to get in contact with you about possibly using your api for a pet project of mine and my gamer groups. I think I tried a while ago to send a message on the webpage but it never went through. What's the best wway to get in touch?

pacMakaveli commented 1 month ago

vlad [at] games [dot] directory

pradella commented 1 month ago

@pradella correct. My database spans 9 years of constantly syncing the API and more importantly, users. One easy way to map these is to scan someone that you know has completed/owns lots of games. Hakoom, Ikemenzi etc.. (https://psnprofiles.com/dav1d_123) you can get their account ids from the search endpoint.

Again, if there's a way I can provide this data to you via games.directory's API, please let me know. I'd be more than happy as one of my goals with this is to provide little snippets of APIs for these kind of use cases. (obviously free, just in case it's not clear)

Such an inspiration you are, thanks bro @pacMakaveli

pacMakaveli commented 1 month ago

Also, feel free to join https://discord.gg/k4QRsfmA . It'll be easier to communicate and plan ahead.