SoulSeek Flows
This document describes different flows and details for the SoulSeek protocol
How to read this document
The order of the actions is (usually) deliberate to keep the order of the messages. For example: when a user leaves a room the first step is to remove the user from the list of joined users. If a subsequent step describes that a message should be sent to all joined users then that list excludes the leaving user itself as the user was removed in the first step.
For some of the checks and input checks the action performed is simply “Continue”, in these cases the behaviour has been verified but no error is given. This is usually done in cases where an error was expected but non was given and is a reminder that it has been verified.
Peer Flows
All peer connections use the TCP protocol. The SoulSeek protocol defines 3 types of connections with each its own set of messages. The type of connection is established during the peer initialization message:
Peer (
P) : Peer to peer connection for messagingDistributed (
D) : Distributed network connectionsFile (
F) : Peer to peer connection for file transfer
To accept connections from incoming peers there should be at least one listening port opened. However newer clients will open two ports: a non-obfuscated and an obfuscated port.
The obfuscated port is not mandatory and is usually just the obfuscated port + 1. Normally any port can be picked, both ports can be made known to the server using the SetListenPort (Code 2) message. How obfuscation works is described in the Obfuscation section
When a peer connection is accepted on the obfuscated port all messaging should be obfuscated with
each their own key, this only applies to peer connection though (P). Distributed (D) and
file (F) connections are not obfuscated aside from the Peer Initialization Messages.
Connecting to a peer
There are two ways a peer connection can be established: a direct connection to the peer or an indirect connection using the server as a middle man.
The original flow was to first attempt a direct connection and if that fails fall back to an indirect connection (fallback method). However this can be very slow and most clients opt to try both methods in parallel and use whichever connection succeeds first (race method).
Direct Connection
Request the IP address and listening ports for the target peer from the server using the GetPeerAddress (Code 3) message
In case the peer exists the server will respond with:
ip_address: IP address of the peerport: listening port of the peerobfuscated_port: obfuscated listening port of the peer (0if not set)
Establish a TCP connection to the peer using
ip_addressandport(orobfuscated_portif using obfuscation)In case of success: send PeerInit (Code 1) over the peer connection
username: our usernameconnection_type: the connection type we want to establish (P, D or F)
Note
The GetPeerAddress (Code 3) can return address 0.0.0.0 as ip_address which indicates the peer
is not connected to the server
Indirect Connection
Generate a ticket number
Send ConnectToPeer (Code 18) to the server:
ticket: the generated ticketusername: target peer usernameconnection_type: the connection type we want to establish (P, D or F)
The server will check if the target peer is connected:
If he is connected the server will send the ConnectToPeer (Code 18) message with the following information:
ticket: the generated ticketusername: our usernameconnection_type: (P, D or F)ip_address: our IP addressport: our listening portobfuscated_port: our obfuscated listening port (0if not set)
If he is not connected the server will send a CannotConnect (Code 1001) message back to us and the flow ends:
ticket: the generated ticketusername: <empty>
Target peer attempts to establish a TCP connection to us using the provided
ip_addressandport(orobfuscated_portif using obfuscation)In case of success: target peer sends PeerPierceFirewall (Code 0) over the peer connection
ticket: the generated ticket
In case of failure:
Target peer sends CannotConnect (Code 1001) to the server
ticket: the generated ticket
We receive CannotConnect (Code 1001) from the server
ticket: the generated ticket
Note
When building a client the desired username and connection type should be stored alongside the generated ticket to be able to match it when the incoming peer connection sends the PeerPierceFirewall (Code 0) message.
Delivering Search Results
Delivery of search results is the same process for all kinds of search messages:
Receive FileSearch (Code 26) from the server
ticketusername
If the query matches:
Initialize peer connection (
P) for theusernamefrom the request
ticketfrom the original search request and query matches
Obfuscation
Obfuscation is possible only through the peer connection type (P) and the peer initialization
messages (PeerInit (Code 1) and PeerPierceFirewall (Code 0)). If a distributed or file connection is
made only the initialization messages can be obfuscated, after that the client should switch to
sending/received messages non-obfuscated.
When messages are obfuscated the first 4 bytes are the key which is randomly generated for each message. To decode the first 4 bytes of the actual message the following steps should be taken:
Convert the key to an integer (from little-endian)
Perform a circular shift of 31 bits to the right
Convert back to bytes (to little-endian)
XOR the first 4 bytes with the 4 bytes of the rotated key
For the next 4 bytes, perform the same operation but rotate the resulting key again.
Example
Original message :
08000000 79000000 e8030000Obfuscated message :
1494ee4a 2028dd95 2850ba2b 4aa37457(first 4 bytes are the key)
De-obfuscated first 4 bytes of the message
Convert the key to big-endian: 14 94 ee 4a -> 4a ee 94 14
Original key:
Hex:
4a ee 94 14Bin:
0100 1010 1110 1110 1001 0100 0001 0100
Key shifted 31 bits to the right:
Hex:
95 dd 28 28Bin:
1001 0101 1101 1101 0010 1000 0010 1000
Convert to little-endian: 95 dd 28 28 -> 28 28 dd 95
XOR the first 4 bytes of the message (20 28 dd 95) with the rotated key:
b3 |
b2 |
b1 |
b0 |
|
|---|---|---|---|---|
28 |
28 |
dd |
95 |
|
XOR |
20 |
28 |
dd |
95 |
08 |
00 |
00 |
00 |
De-obfuscated second 4 bytes of the message
Convert the key to big-endian: 28 28 dd 95 -> 95 dd 28 28
Original key:
Hex:
95 dd 28 28Bin:
1001 0101 1101 1101 0010 1000 0010 1000
Key shifted 31 bits to the right:
Hex:
2b ba 50 51Bin:
0010 1011 1011 1010 0101 0000 0101 0001
Convert to little-endian: 2b ba 50 51 -> 51 50 ba 2b
XOR the second 4 bytes of the message (28 50 ba 2b) with the rotated key:
b3 |
b2 |
b1 |
b0 |
|
|---|---|---|---|---|
51 |
50 |
ba |
2b |
|
XOR |
28 |
50 |
ba |
2b |
79 |
00 |
00 |
00 |
Third 4 bytes
Convert the key to big-endian: 51 50 ba 2b -> 2b ba 50 51
Original key:
Hex:
2b ba 50 51Bin:
0010 1011 1011 1010 0101 0000 0101 0001
Key shifted 31 bits to the right:
Hex:
57 74 a0 a2Bin:
0101 0111 0111 0100 1010 0000 1010 0010
Convert to little-endian: 57 74 a0 a2 -> a2 a0 74 57
XOR the third 4 bytes of the message (4a a3 74 57) with the rotated key:
b3 |
b2 |
b1 |
b0 |
|
|---|---|---|---|---|
a2 |
a0 |
74 |
57 |
|
XOR |
4a |
a3 |
74 |
57 |
e8 |
03 |
00 |
00 |
Delivering Search Results
Delivery of search results is the same process for all kinds of search messages:
FileSearch (Code 26) from the server in case the searcher used UserSearch (Code 42) or RoomSearch (Code 120)
ServerSearchRequest (Code 93) from the server in case we are branch root
DistributedServerSearchRequest (Code 93), DistributedSearchRequest (Code 3) from the distributed peer in case we are in the distributed network
Receive search request. All messages contain:
ticketusernamequery
If the query matches:
Initialize peer connection (
P) for theusernamefrom the request
ticket: from the original search request and file data
Distributed Flows
Obtaining a parent
When ToggleParentSearch (Code 71) is enabled then every 60 seconds the server will send the client a PotentialParents (Code 102) command (containing a maximum of 10 possible parents) until we disable our search for a parent using the ToggleParentSearch (Code 71) command. The PotentialParents (Code 102) command contains a list with each entry containing: username, IP address and port. Upon receiving this command the client will attempt to open up a connection to each of the IP addresses in the list to find a suitable parent.
After establishing a distributed connection with one of the potential parents the peer will send out a DistributedBranchLevel (Code 4) and optionally, if the branch level is non-zero, DistributedBranchRoot (Code 5) over the distributed connection. If the peer is selected to be the parent the other potential parents are disconnected and the following messages are then send to the server to let it know where we are in the hierarchy:
BranchLevel (Code 126) : BranchLevel from the parent + 1
BranchRoot (Code 127) : The BranchRoot received from the parent as-is
ToggleParentSearch (Code 71) : Setting to false disables receiving PotentialParents (Code 102) messages
AcceptChildren (Code 100): See Max children setting
Once the parent is set it will start sending us search requests or if we are branch root the server will send us search requests.
Note
The implementation currently differs from the original clients. The implementation will make the first peer that sends a DistributedBranchLevel (Code 4) and DistributedBranchRoot (Code 5) (except if level was 0, see above). Others clients decide the parent based on the first search request received.
List of open questions:
If the parent is disconnected, are the children disconnected as well? If no, are the new branch root/level values re-advertised?
Obtaining children
The AcceptChildren (Code 100) command tells the server whether we want to have any children, this is used in combination with the ToggleParentSearch (Code 71) command which enables searching for parents. Enabling it will cause us to be listed in PotentialParents (Code 102) commands sent to other peers. It is not mandatory to have a parent and to obtain children if we ourselves are the branch root (branch level is 0).
The process is very similar to the one to obtain a parent except that this time we are in the role of the other peer; we need to advertise the branch level and branch root using the DistributedBranchLevel (Code 4) and DistributedBranchRoot (Code 5) commands as soon as another peer establishes.
Max children
Clients limit the amount of children depending on the upload speed that is currently stored on the server. Whenever a GetUserStats (Code 36) message is received (for the logged in user) this limit is re-calculated and depends on the ParentSpeedRatio (Code 84) and ParentMinSpeed (Code 83) values the server sent after logon.
When a client receives a GetUserStats (Code 36) message the client should determine whether to enable or disable accepting children and if enabled, calculate the amount of maximum children.
If the
avg_speedreturned is smaller than the value received by ParentMinSpeed (Code 83) * 1024 :Send AcceptChildren (Code 100) (
accept = false)
If the
avg_speedis greater or equal than the value received by ParentMinSpeed (Code 83) * 1024 :Send AcceptChildren (Code 100) (
accept = true)Calculate the
dividerfrom theratioreturned by ParentSpeedRatio (Code 84): (ratio/ 10) * 1024Calculate the max number of children : floor(
avg_speed/divider)
Example calculations
Calculation 1
Values:
ratio=50
avg_speed=20480
Calculation:
(50 / 10) * 1024 = 5120
floor(20480 / 5120) = 4
Calculation 2
Values:
ratio=30
avg_speed=20480
Calculation:
(30 / 10) * 1024 = 3072
floor(20480 / 3072) = 6 (floored from 6.66666666)
Note
With this formula there is a possibility that even when the avg_speed is greater than the
min_speed_ratio the max amount of children calculated is 0. In this case the
AcceptChildren (Code 100) is sent with accept=true, despite the client not accepting any children
Searches on the distributed network
Searches for the branch root (level = 0) will come from the server in the form of a ServerSearchRequest (Code 93) message. The branch root forwards this message as-is directly to its children (level = 1). The children will then convert this message into a DistributedSearchRequest (Code 3) and pass it on to its children (level = 2). It is up to the peer to perform the query on the local filesystem and report the results the peer making the query.
Note
The reason why it is done this way is not clear. The branch root could perfectly convert it into a DistributedSearchRequest (Code 3) itself before passing it on. This would in fact be cleaner as right now the DistributedServerSearchRequest (Code 93) is just a copy of ServerSearchRequest (Code 93), otherwise this wouldn’t parse.
The naming of these messages is probably incorrect as the distributed_code parameter of the
ServerSearchRequest (Code 93) holds the distributed message ID. Possibly the server could send any
distributed command through this that needs to be broadcast over the distributed network.
Transfer Flows
Basic Flow
For downloading we need only the username and filename returned by a PeerSearchReply (Code 9).
Request a file download over a peer connection (P):
Downloader: PeerTransferQueue (Code 43)
filename: name of the file to download
The uploader should queue the download request. He decides when the flow continues:
Uploader: PeerTransferRequest (Code 40) : this can sent over any peer connection (
P)direction:1ticketfilesize
Downloader: PeerTransferReply (Code 41)
allowed:true
The uploader should open a new file connection (F) to the downloader and proceed with the file
transfer:
Uploader:
ticket(uint32) : should be the same ticket as the PeerTransferRequest (Code 40) messageDownloader:
offset(uint64) : indicates from which byte the uploader should start sending dataUploader: Send file data
Downloader: Close connection when all data is received
Uploader: will send a SendUploadSpeed (Code 121) message to the server with the average upload speed
Warning
It is up to the downloader to close the file connection, the downloader confirms he has received all bytes by closing. If the uploader closes the connection as soon as all data is sent the file will be incomplete on the downloader side.
Deprecated: Uploads
Note
This section describes a flow which is no longer used but is kept for reference. It describes a flow for uploading files to another user without a prior download request (pushing a file). This is only implemented by SoulSeekNS.
Successful upload
Uploader opens a new peer connection (P):
Uploader: PeerUploadQueueNotification (Code 52)
Uploader: PeerTransferRequest (Code 40)
direction:1filenamefilesize
Downloader: PeerTransferReply (Code 41)
allowed:true
Uploader opens a new file connection (F) and proceeds with uploading
Note
It seems like the PeerUploadQueueNotification (Code 52) is stored as subsequent uploads do not require this message to be sent
Upload not allowed
Uploader opens a new peer connection (P):
Uploader: PeerUploadQueueNotification (Code 52)
Uploader: PeerTransferRequest (Code 40)
direction:1filenamefilesize
Downloader: PeerTransferReply (Code 41)
allowed:falsereason:Cancelled
Uploader: PeerUploadFailed (Code 46)
filename
Server Connection and Logon
SoulSeekQt: server.slsknet.org:2416
SoulSeek 157: server.slsknet.org:2242
Establishing a connection and logging on:
Open a TCP connection to the server
Open up at least one listening connection to allow incoming peer connections
Send the Login (Code 1): message on the server socket
A login response will be received which determines whether the login was successful
After the response we send the following messages back to the server with some information about us:
CheckPrivileges (Code 92) : Check if we have privileges
SetListenPort (Code 2) : The listening port(s), obfuscated and non-obfuscated
SetStatus (Code 28) : Our status (offline, away, available)
SharedFoldersFiles (Code 35) : Number of directories and files we are sharing
AddUser (Code 5) : Using our own username as parameter
We also send messages to advertise we have no parent:
ToggleParentSearch (Code 71) : Should initually be true
BranchRoot (Code 127) : Initially our own username
BranchLevel (Code 126) : Initially should be
0AcceptChildren (Code 100) : Accept child connections
After connection is complete, send a Ping (Code 32) command to the server every 5 minutes.
Server Flows
This section describes the flows from a point of view of the server as well as the presumed internal structures.
Structures
Server structure
Field |
Type |
Default |
Description |
|---|---|---|---|
peers |
array[Peer] |
<empty> |
Current peer connections to the server |
users |
array[User] |
<empty> |
List of users |
rooms |
array[Room] |
<empty> |
List of rooms |
distributed_tree |
map[string, DistributedValues] |
<empty> |
Distributed values for each logged on user. The key is the name of the user these values belong to |
privileged_users |
array[string] |
<empty> |
List of names of privileged users |
excluded_search_phrases |
array[string] |
<empty> |
|
motd |
string |
<empty> |
Message of the day |
parent_min_speed |
integer |
1 |
|
parent_speed_ratio |
integer |
50 |
|
min_parents_in_cache |
integer |
10 |
|
parent_inactivity_timeout |
integer |
300 |
|
search_inactivity_timeout |
integer |
0 |
|
distributed_alive_interval |
integer |
0 |
|
wishlist_interval |
integer |
720 |
Peer structure
Field |
Type |
Default |
Description |
|---|---|---|---|
user |
User |
<not set> |
User structure assigned to this peer connection |
ip_address |
string |
0.0.0.0 |
IP address that the peer is connecting from |
QueuedPrivateMessage structure
Field |
Type |
Default |
Description |
|---|---|---|---|
username |
string |
<not set> |
Username of the sender of the private message |
message |
string |
<not set> |
Message body |
chat_id |
int |
<not set> |
Generated chat ID |
timestamp |
int |
<not set> |
Timestamp that the send sent the message to the server |
DistributedValues structure
Field |
Type |
Default |
Description |
|---|---|---|---|
root |
string |
<not set> |
Username of the distributed root |
level |
int |
0 |
|
child_depth |
int |
0 |
UserStatus enumeration
List of possible user statuses
Status |
Value |
|---|---|
OFFLINE |
0 |
AWAY |
1 |
ONLINE |
2 |
User structure
Structure of a user:
Field |
Type |
Default |
Description |
|---|---|---|---|
name |
string |
||
password |
string |
<empty> |
|
is_admin |
boolean |
false |
|
status |
integer |
0 |
Current status of the user. Possible values described in the user status table |
avg_speed |
integer |
0 |
Average upload speed |
uploads |
integer |
0 |
|
shared_file_count |
integer |
0 |
|
shared_folder_count |
integer |
0 |
|
country |
string |
<empty> |
|
interests |
array[string] |
<empty> |
|
hated_interests |
array[string] |
<empty> |
|
added_users |
array[string] |
<empty> |
List of users added through the AddUser (Code 5) message |
enable_private_rooms |
boolean |
false |
|
enable_parent_search |
boolean |
false |
|
enable_public_chat |
boolean |
false |
|
accept_children |
boolean |
false |
|
port |
integer |
0 |
|
obfuscated_port |
integer |
0 |
Room structure
Field |
Type |
Description |
|---|---|---|
name |
string |
Name of the room |
tickers |
map[string, string] |
Ordered map of room tickers. Key=username, value=ticker |
joined_users |
array[string] |
List of users currently in the room |
registered_as_public |
boolean |
Tracks if the room was ever registered as public (default: false) |
owner |
array[string] |
[Optional] Private Rooms. Owner of the room |
members |
array[string] |
[Optional] Private Rooms. Members of the room (excludes owner) |
operators |
array[string] |
[Optional] Private Rooms. Users with operator privileges |
Calculated values:
Field |
Type |
Description |
|---|---|---|
status |
RoomStatus |
Returns the current status of the room with 3 possible values:
* RoomStatus.PRIVATE : If |
all_members |
array[string] |
Returns the list of members including the owner (if there is any) |
Warning
It’s important to understand that rooms never get immediately destroyed (possibly they do get cleaned up after some time has passed without activity).
If a room was public it cannot be claimed as a private room
If ownership is dropped for a private room the owner, members and operators values simply get reset and all joined_users except for the owner get kicked. This effectively makes the private room a public room until the owner leaves, at which point the room becomes unclaimed and can be claimed again as a private or public room.
Events
Peer Connected
Actors:
peer: A peer connection
Actions:
Create a new
Peerstructure and add it to the list ofpeers
Note
If the peer does not perform a valid logon with 1 minute then the peer will be disconnected
TODO: Check if performing an invalid logon extends the timeout
TODO: If above is true, check whether the same happens with other messages
TODO: Check what happens if any other message except logon is sent
TODO: Check total timeout of the server. Normally a ping should be sent every 5 minutes, if this is not done and no other messages are sent will the client be disconnected as well?
Peer Disconnected
Actors:
peer: A peer connection
Actions:
Remove the
peerfrom thepeerslistIf the
peerhad auserstructure assignedFor each
roomwhere the user is in thejoined_users-
status :
UserStatus.OFFLINE
Protocol Message Received
Actors:
peer: A peer connection over which a valid protocol message was sent
Checks:
Actions:
Messages
Login
The Login (Code 1) message is the first message a peer needs to send to the server
Message Login (Code 1)
Actors:
user: The user attempting to login
Input Checks:
If
client_versionis less than TBDFunction: Server Notification Message : message : “Your connection is restricted: You cannot search or chat. Your client version is too old. You need to upgrade to the latest version. Close this client, download new version from http://www.slsknet.org, install it and reconnect.”
If
usernameis empty:Send Login (Code 1)
success : false
reason :
INVALIDPASS
If
passwordis empty : Continue (there are some reference to a fail reason calledEMPTYPASSWORDbut isn’t used)If
md5hashparameter mismatches with the MD5 hash of theusername + passwordparameters : Continue
Checks:
If the user exists in the
userslist:If the
passwordparameter of the message does not equal thepasswordof theuser:Send Login (Code 1)
success : false
reason :
INVALIDPASS
If there is
peerin thepeerslist with theuseralready assigned:Send Kicked (Code 41) message to the existing peer
Disconnect the existing peer
Actions:
If the user does not exist in the
userslist:Create a new user and add it to the
userslist
Assign the
userto thepeerstructure-
status :
UserStatus.ONLINE
Send to the
user:-
success : true
greeting : value from
motdmd5hash : md5hash of the
passwordof theuser
ParentMinSpeed (Code 83) : value from
parent_min_speedParentSpeedRatio (Code 84) : value from
parent_speed_ratioWishlistInterval (Code 104) : value from
wishlist_intervalPrivilegedUsers (Code 69) : list of
privileged_usersExcludedSearchPhrases (Code 160) : list of
excluded_search_phrases
-
Set Listening Ports
Message: SetListenPort (Code 2)
Input Checks:
Checks:
TODO: If initially the obfuscated port was set and not provided the second time, what will the value be? (and vica versa)
Actions:
Set
portandobfuscated_portof theuser
Get Peer Address
Message: GetPeerAddress (Code 3)
Actions:
If there is a peer in the
peerslist with the user assigned-
username: theusernamefor which the peer address was requestedip:Peer.ip_addressport:User.porthas_obfuscated_port: 0 if theobfuscated_portis 0 otherwise 1obfuscated_port:User.obfuscated_port
-
If there is a no peer in the
peerslist with the user assigned-
username: theusernamefor which the peer address was requestedip:0.0.0.0port:0has_obfuscated_port:0obfuscated_port:0
-
Set Status
A request to change the current status of the user
Message: SetStatus (Code 28)
Actors:
user: User attempting to change his status
Input checks:
If the
statusis notUserStatus.ONLINEorUserStatus.AWAY: Do nothing
Checks:
If the requested
statusequals thestatusof theuser: Do nothing
Actions:
-
status :
statusfield from the message
Send Upload Speed
A request to update the average upload speed of the user
Message: SendUploadSpeed (Code 121)
Actors:
user: User attempting to change his sharing stats
Actions:
Consider speed being the speed value sent in the message
Change the
userstructureCalculate the
avg_speed(floor the result):\[avgspeed = ((avgspeed * uploads) + speed) / (uploads + 1)\]Increase the
uploadsby 1
Get User Status
Message: GetUserStatus (Code 7)
Checks:
If the user does not exist in the
userslist:-
username: name of the user for which status was requestedstatus:UserStatus.OFFLINE
-
Actions:
username: name of the user for which status was requested
status:statusof the user for which status was requested
Get User Stats
Message: GetUserStats (Code 36)
Checks:
If the user does not exist in the
userslist:-
username: name of the user for which stats were requestedall stats set to 0
-
Actions:
username: name of the user for which stats were requestedstats of the user for which stats were requested
Add A User
Message: AddUser (Code 5)
Actors:
adder: User adding a user to hisadded_usersaddee: User to be added to theadded_users
Checks:
If the
addeedoes not exist in the list ofusers-
username: name of theaddeeexists: false
-
Actions:
If the
adderandaddeeare different (not adding self):If the
addeeis not yet in the list ofadded_usersof theadderAdd the
addeeto the list ofadded_usersof theadder
-
username: name of theaddeeexists: trueRest of the field are filled in with the values of the
addee
Remove A User
Message: RemoveUser (Code 6)
Actors:
remover: User removing a user from hisadded_usersremovee: User to be removed from theadded_users
Checks:
If the
removerandremoveeare the same : Do nothingIf the
removeeis not in the list ofadded_usersof theremover: Do nothing
Actions:
Remove the
removeefrom the list ofadded_usersof theremover
Private Chat Message
This message is used to send a private chat message to a single user.
Message: PrivateChatMessage (Code 22)
Actors:
sender: User sending the messagereceiver: User to which the message should be sent
Input Checks:
If the
usernameis empty : Continue (it should not be possible to have a user with an empty username)If the
messageis empty : Continue
Checks:
If the
receiverdoes not exist : Do nothingIf the
senderis thereceiver: Continue
Actions:
Generate a
chat_idCreate a new instance of
QueuedPrivateMessageand add to thequeued_private_messagesof thereceiverchat_id : Generated
chat_idtimestamp : Current timestamp
message :
messagevalue of the messageusername : Value of the
namevalue of thesender
If there is a
peerwhich is associated with thereceiver:Send to the
receiver-
chat_id : Generated
chat_idtimestamp : Current timestamp
message :
messagevalue of the messageusername : Value of the
namevalue of thesenderis_direct : true
-
Private Chat Message Acknowledge
This is used to acknowledge that a specific private message has been received. If private messages are not acknowledged they will be resent when the user logs in again.
Message: PrivateChatMessageAck (Code 23)
Actors:
receiver: the user who has received the private message and is using this message to acknowledge he has received it
Actions:
If the
chat_idexists in thequeued_private_messagesof thereceiverRemove the message with
chat_idfrom thequeued_private_messageof thereceiver
Private Chat Message Multiple Users
Message: PrivateChatMessageUsers (Code 149)
Actors:
sender: User sending the message to multiple users
Input Checks:
If the
messageis empty : ContinueIf the list of
usernamesis empty : Continue
Actions:
For each
receiverin theusernamesparameter of the messageIf the
receiverdoes not exist : Do nothingIf the
receiverexists:If the
senderis not in the list ofadded_userof thereceiver: Do nothingCreate a new instance of
QueuedPrivateMessageand add to thequeued_private_messagesof thereceiverchat_id : Generated
chat_idtimestamp : Current timestamp
message :
messagevalue of the messageusername : Value of the
namevalue of thesender
If there is a
peerwhich is associated with thereceiver:Send to the
receiver-
chat_id : Generated
chat_idtimestamp : Current timestamp
message :
messagevalue of the messageusername : Value of the
namevalue of thesenderis_direct : true
-
Set Distributed Root
Message: BranchRoot (Code 127)
Actors:
user: User setting his distributed root
Checks:
If the user with given
usernamedoes not exist or is offline
Actions:
Assign the
usernameto thedistributed_tree[user.name].rootfield
Set Distributed Level
Message: BranchLevel (Code 126)
Actors:
user: User setting his distributed level
Actions:
Assign the
levelto thedistributed_tree[user.name].levelfield
Set Child Depth
Message: DistributedChildDepth (Code 7)
Actors:
user: User setting the child depth
Actions:
Assign the
depthto thedistributed_tree[user.name].child_depthfield
Note
It’s not actually known what the server does with this value, possibly it only sets the child_depth value if a value is received that is greater than what is currently stored
Global Search
Perform a search query to everyone on the network.
Message: FileSearch (Code 26)
Actors:
searcher: User requesting a global search
Actions:
Foreach user who is a distributed root:
Room Search
Performs a search on every one in a single room:
Searcher send: RoomSearch (Code 120) : with a ticket, the query and room name
Room user receive: FileSearch (Code 26)
Message: RoomSearch (Code 120)
Actors:
searcher: User requesting a room search
Checks:
TODO: If the room does not exist
TODO: If the user not joined to the room
TODO: If the query is empty
Actions:
Foreach user in the list of
joined_users:-
username: name of thesearcherticket:ticketparameter of the request messagequery:queryparameter of the request message
-
Note
TODO: Verify if request is also sent to self
TODO: Verify if it is possible to query non-joined private rooms of which we are member
User Search
Performs a search query on an individual user
Message: RoomSearch (Code 120)
Actors:
searcher: User requesting to query an individualsearchee: User being queried
Checks:
TODO: User does not exist
Actions:
To
searchee:-
username: name of thesearcherticket:ticketparameter of the request messagequery:queryparameter of the request message
-
Room List
The room list is received after login but can be refreshed by sending another RoomList (Code 64) request.
Message: RoomList (Code 64)
Actions:
-
rooms: public roomsrooms_private_owned: private rooms for which we areownerrooms_private: private rooms for which we are in thememberslistrooms_private_operated: private rooms for which we are in theoperatorslist
Note
Not all public rooms are listed in the initial RoomList (Code 64) message after login; only rooms with 5 or more joined_users.
It’s not clear where this limit comes from, and possibly if the total amount of public rooms is low those rooms are included anyway (and perhaps if it’s high the minimum amount of members increases as well)
Room Joining / Creation
Joining and creating the room is combined into one message, it contains the name of the room and whether the room is private
Message: JoinRoom (Code 14)
Actors:
joiner: user requesting to join the room
Input Checks:
If room
nameis empty:Function: Server Notification Message : message : “Could not create room. Reason: Room name empty.”
If room
namecontains leading or trailing white spaces:Function: Server Notification Message : message : “Could not create room. Reason: Room name
namecontains leading or trailing spaces.”
If room
namecontains multiple subsequent white spaces (eg.: “my<2 or more spaces>room”):Function: Server Notification Message : message : “Could not create room. Reason: Room name
namecontains multiple following spaces.”
If room
namecontains non-ascii characters:Function: Server Notification Message : message : “Could not create room. Reason: Room name
namecontains invalid characters.”
Checks:
If room exists and
joineris in thejoined_userslist (user already joined):Do nothing
If room exists and
joineris not in theall_memberslist:Function: Server Notification Message : message : “The room you are trying to enter (
name) is registered as private.”
Actions:
If the room does not exist (or room is unclaimed) and the request is to join a public room (
private=false):Create new room or claim the unclaimed room
name: set to desired nameowner: leave emptyregistered_as_public: true
If the room is unclaimed, is registered as public room (
registered_as_public=true) and the request is to join a private room (private=true):Function: Server Notification Message : message to
joiner: “Room (name) is registered as public.”
If the room does not exist (or room is unclaimed), is not registered as public room (
registered_as_public=false) and the request is to join a private room (private=true):Create new room or claim the unclaimed room
name: set to desired nameowner: set to room creatorregistered_as_public: false
Leave Room
Message: LeaveRoom (Code 15)
Actors:
leaver: user requesting to leave the room
Checks
User is not part of the room : Do nothing
Room does not exist : Do nothing
Actions
Grant Membership in Private Room
Operators and owners of a private room can grant membership to a user allowing that user to join the room. The message contains the name of the room and the username of the user that should be given membership.
Message: PrivateRoomGrantMembership (Code 134)
Actors:
granter: User granting membershipgrantee: User being granted membership
Checks:
If the room
nameis not a valid room (public) : Do nothingIf the room
nameis not a valid room (does not exist) : Do nothingIf the
granteeandgranterare the same : Do nothingIf the
granteris not theowneror in theoperatorslist : Do nothingIf the
granteeis offline or does not exist:Function: Server Notification Message : message : “user
granteeis not logged in.”
If the
granteeis not accepting private room invites:Function: Server Notification Message : message : “user
granteehasn’t enabled private room add. please message them and ask them to do so before trying to add them again.”
If the
granteris inoperatorslist and tries to add theownerFunction: Server Notification Message : message : “user
granteeis the owner of roomname”
If the
granteeis already in thememberslist:Function: Server Notification Message : message : “user
granteeis already a member of roomname”
Actions:
Revoke Membership from Private Room
Removes a member from a private room. The owner can remove operators and members, operators can only remove members. The message contains the name of the room and the username of the user that should be revoked membership.
Message: PrivateRoomRevokeMembership (Code 135)
Actors:
revoker: User revoking membershiprevokee: User being revoked membership
Checks:
If the
revokerandrevokeeare the same user : Do nothingIf the
revokeeis not in thememberslist : Do nothingIf the
revokeris in theoperatorslist and therevokeeis theowner: Do nothingIf the
revokerandrevokeeare both in theoperatorslist : Do nothing
Actions:
Granting Operator Privileges in a Private Room
Room owners can grant operator privileges to members of a private room, the message contains the name of the room and the username of the member that should be granted operator privileges.
Message: PrivateRoomGrantOperator (Code 143)
Actors:
granter: User granting the operator privilegesgrantee: User having operator privileges granted
Checks:
If
granteris not theowner: Do nothingIf user does not exist or is offline (even in
memberslist):Function: Server Notification Message : message : “user
granteeis not logged in.”
If
granteeis not in thememberslist:Function: Server Notification Message : message : “user
granteemust first be a member of roomname”
If
granteeis already in theoperatorslist:Function: Server Notification Message : message : “user
granteeis already an operator of roomname”
Actions:
Revoking Operator Privileges in a Private Room
Room owners can revoke operator privileges from operator of a private room. The message contains the name of the room and the username of the member that should have his operator privileges revoked.
Message: PrivateRoomRevokeOperator (Code 144)
Actors:
revoker: User revoking the operator privilegesrevokee: User having operator privileges revoked
Checks:
If user does not exist: Do nothing
If
revokeris not theowner: Do nothingIf
revokeeis not in thememberslist: Do nothingIf
revokeeis not in theoperatorslist: Do nothing
Actions:
Dropping Membership
Members themselves can drop their membership from a private room
Message: PrivateRoomDropMembership (Code 136)
Checks:
If the user is not in the
memberslist : Do nothing
Actions:
If the user is in the
operatorslist:
Dropping Ownership
Owners can drop ownership of a private room, this will disband the private room.
Message: PrivateRoomDropOwnership (Code 137)
Checks:
If the user tries to drop ownership for a room that does not exist: Do nothing
If the user tries to drop ownership of a public room: Do nothing
If the member is not the
owner
Actions:
Reset the
ownerEmpty the
operatorslist of the roomEmpty the
memberslist of the room but keep a reference to this listFor each member in the stored list:
Warning
The server will not remove the the owner from the joined_users. The owner should send a second command to leave the room after sending this command. This seems like a mistake that was corrected in the client itself instead of on the server side.
Room Message
Sending a chat message to a room is done through the RoomChatMessage (Code 13) message
Message: RoomChatMessage (Code 13)
Actors:
sender: User sending a chat message to the room
Checks:
If the room does not exist : Do nothing
If the user is not in the
joined_userslist : Do nothing
Actions:
For each user in the
joined_userslist:RoomChatMessage (Code 13) : with
sender,messageand roomname
If the room is public (
is_private=false):For each user currently online and has public chat enabled:
PublicChatMessage (Code 152) : with
sender,messageand roomname
Note
Empty message is allowed
Set Room Ticker
Room tickers are a sort of room wall, where users can place a single message that is visible to everyone in the room. They are sent after the user joins a room using the RoomTickers (Code 113) message and users will be notified of updates through the RoomTickerAdded (Code 114) and RoomTickerRemoved (Code 115) messages.
A room ticker can be set with the SetRoomTicker (Code 116) message for which the actions are described in this section.
Message: SetRoomTicker (Code 116)
Actors:
user: User that requests to set a room ticker
Input Checks:
If the length of the
tickeris greater than 1024 : Do nothingTODO: Any characters not allowed?
Checks:
If the room does not exist : Do nothing
If
useris not in thejoined_userslist (public) : Do nothing
Actions:
If the
userhas an entry intickersRemove the entry of the
userfrom thetickersFor each user in the
joined_userslist:RoomTickerRemoved (Code 115) : with room
nameand theuserfor which the ticker was removed
If the
tickerin the SetRoomTicker (Code 116) message is not empty:Add an entry for the
userto thetickersFor each user in the
joined_userslist:RoomTickerAdded (Code 114) : with room
name,userand theticker
Note
Tickers are retained even when ownership is dropped for a private room
Enable Public Chat
Message: EnablePublicChat (Code 150)
Actions:
Set
enable_public_chattotrue
Disable Public Chat
Message: EnablePublicChat (Code 150)
Actions:
Set
enable_public_chattofalse
Add an Interest
Message: AddInterest (Code 51)
Input Checks:
If the
interestis empty : Continue
Actions:
If the
interestis not present in the list ofinterests
Add the
interestto the list ofinterests
Add a Hated Interest
Message: AddHatedInterest (Code 117)
Input Checks:
If the
hated_interestis empty : Continue
Actions:
If the
hated_interestis not present in the list ofhated_interestsAdd the
hated_interestto the list ofhated_interests
Remove an Interest
Message: RemoveInterest (Code 52)
Input Checks:
If the
interestis empty: Continue
Actions:
If the
interestis present in the list ofinterestsRemove the
interestfrom the list ofinterests
Remove a Hated Interest
Message: RemoveHatedInterest (Code 118)
Input Checks:
If the
hated_interestis empty: Continue
Actions:
If the
hated_interestis present in the list ofhated_interestsRemove the
hated_interestfrom the list ofhated_interests
Getting Interests
Message: GetUserInterests (Code 57)
Actors:
user: user for which the interests were requested
Input Checks:
If the
usernameis empty: continue
Checks:
If the
userdoes not exist:-
username: username of the user for which interests were requestedinterests: empty listhated_interests: empty list
-
Actions:
-
username: username of the user for which interests were requestedinterests:interestsof theuserhated_interests:hated_interestsof theuser
Get Global Recommendations
Requests the global recommendations
Message: GetGlobalRecommendations (Code 56)
Actions:
Keep a counter (initially empty) to keep track of a score for each of the recommendations:
Loop over all currently active users (including the current user)
Increase the
scorefor all of theinterestsof the other user by 1Decrease the
scorefor all of thehated_interestsof the other user by 1
Unverified: Keep only recommendations where the
scoreis not 0The returned message will contain 2 lists:
The recommendations sorted by score descending (limit to 200)
The unrecommendations sorted by score ascending (limit to 200)
Get Item Recommendations
Request a set of recommendations can be returned based on the specified item.
Message: GetItemRecommendations (Code 111)
Input Checks:
If the
itemis empty : Continue
Actions:
Keep a counter (initially empty) to keep track of a score for each of the recommendations:
Loop over all currently active users (excluding the current user)
If the
itemis in the user’s interests:Increase the
scorefor all of theinterestsof the other user by 1Decrease the
scorefor all of thehated_interestsof the other user by 1
Keep only recommendations where the
scoreis not 0The returned message will contain 1 lists:
The recommendations sorted by score descending (limit to 100)
Get Recommendations
Request a personalized set of recommendations can be returned based on the interests and hated interests.
Message: GetRecommendations (Code 54)
Actions:
Keep a counter (initially empty) to keep track of a score for each of the recommendations:
Loop over all currently active users (excluding the current user)
Loop over all the current user’s
interestsIf the current interest is in the
interestsof the other user:Increase the
scorefor all of theinterestsof the other user by 1Decrease the
scorefor all of thehated_interestsof the other user by 1
If the current interest is in the
hated_interestsof the other userDecrease the
scorefor all of theinterestsof the other user by 1
Loop over all the current user’s
hated_interestsIf the current hated interest is in the
interestsof the other user:Decrease the
scorefor all of theinterestsof the other user by 1
Keep only recommendations that are not in the current user’s
interestsorhated_interestsKeep only recommendations where the
scoreis not 0The returned message will contain 2 lists:
The recommendations sorted by score descending (limit to 100)
The unrecommendations sorted by score ascending (limit to 100)
Note
Keep in mind that the recommendations list can (partially) match the unrecommendations list and vice versa if the limit is not reached. Example: if there are 5 items returned those 5 items will be in both lists
Examples
Following examples are to illustrate how the algorithm works:
Example 1
You |
Other |
|
|---|---|---|
Interests |
item1 |
item1 |
item2 |
||
Hated interests |
Recommendations:
item2 (score = 1)
Example 2
You |
Other |
|
|---|---|---|
Interests |
item1 |
|
item2 |
||
Hated interests |
item1 |
item3 |
Recommendations:
item2 (score = -1)
Example 3
You |
Other |
|
|---|---|---|
Interests |
item1 |
item1 |
item2 |
||
Hated interests |
item1 |
item3 |
Recommendations:
item2 (score = -1)
Example 4
You |
Other |
|
|---|---|---|
Interests |
item1 |
item1 |
item2 |
item2 |
|
Hated interests |
item3 |
|
item4 |
Recommendations:
item3 (score = -2)
item4 (score = -2)
Example 5
You |
Other |
|
|---|---|---|
Interests |
item1 |
|
item2 |
||
Hated interests |
item3 |
item3 |
item4 |
Recommendations: Empty list
Example 6
You |
Other |
|
|---|---|---|
Interests |
item3 |
item1 |
item2 |
||
Hated interests |
item3 |
|
item4 |
Recommendations:
item1 (score = -1)
item2 (score = -1)
Example 7
You |
Other |
|
|---|---|---|
Interests |
item1 |
|
item2 |
||
Hated interests |
item1 |
item3 |
item4 |
Recommendations:
item2 (score = -1)
Example 8
You |
Other |
|
|---|---|---|
Interests |
item3 |
item1 |
item2 |
||
Hated interests |
item1 |
item3 |
item4 |
Recommendations:
item2 (score = -2)
Example 9
You |
Other |
|
|---|---|---|
Interests |
item1 |
item1 |
item2 |
item2 |
|
item5 |
||
Hated interests |
item3 |
|
item4 |
Recommendations:
item5 (score = 2)
item3 (score = -2)
item4 (score = -2)
Example 10
You |
Other |
|
|---|---|---|
Interests |
item1 |
item1 |
item2 |
||
Hated interests |
item2 |
Recommendations: Empty list
Get Similar Users
The GetSimilarUsers (Code 110) message returns users that have similar interests to you.
Message: GetSimilarUsers (Code 110)
Actions:
Create an empty list for storing similar users:
Loop over all currently active users (excluding the current user)
Calculate the amount of
interestsin common between the current user and those of the other userIf the overlap is greater than 1, add it to the list of similar users
Return the list of similar users and the amount of interests in common with that user (limit is unknown)
Get Item Similar Users
The GetItemSimilarUsers (Code 112) message returns users that have the interest as provided in the request message (item).
Message: GetItemSimilarUsers (Code 112)
Input Checks:
If the
itemis empty : Continue
Actions:
Create an empty list for storing similar users:
Loop over all currently active users (including the current user)
If the
itemis in theinterestsof the user add it to the list of similar usersReturn the list of similar users (limit is unknown)
Functions
This section describes flows that are re-used in several flows
Function: Update user status
Actors:
user: User updating his status
Actions:
Update the
statusfield of theuserto the requested statusIf the requested
statusis notUserStatus.ONLINEorUserStatus.OFFLINE:Send to
user-
username: the name of theuserstatus: the new status
-
For each user that has the
userin theadded_userslist:-
username: the name of theuserstatus: the new status
-
For each room where the
useris in thejoined_userslist:For each user in the
joined_userslist of that room:-
username: the name of theuserstatus: the new status
-
Note
This function is used in the following 3 situations:
During login: user status goes from offline to online
During disconnect: user status goes from online/away to offline
When user sets status: user status goes from online to away or vice versa
Step 2, informing the user itself of the status update, is effectively only executed when the status is changed to the away status. As such this message is only sent when a user changes his status from online to away. This seems to be a bug in the server.
Function: Send Queued Messages
Actors:
receiveruser that the queued messages need to be delivered to
Actions:
For each messages in the
queued_private_messagesof thereceiver-
chat_id :
chat_idvalue of the queued messagetimestamp :
timestampvalue of the messagemessage :
messagevalue of the queued messageusername :
usernameis_direct : false
-
Note
Messages are not removed from this list until they are acknowledged
Function: Send Room List Update
This is a collection of messages commonly sent to the peer after performing an action on a private room or after logging on.
Actions:
Server: Send Room List
Server: For each private room where the user is
owneror in the list ofmembersPrivateRoomMembers (Code 133) with room_name and list of
members
Server: For each private room where the user is
owneror in the list ofmembersPrivateRoomOperators (Code 148) with room_name and list of
operators
Function: Notify room owner
Function to notify the room owner. This short function sends a server chat message to the
owner of a room if the room has an owner.
Actions:
If the room has its
ownervalue set:Function: Server Notification Message : to
owner:message
Function: Join room
Function to join the room, checks should already be performed.
Actors:
joiner: user requesting to join the room
Actions:
Add the user to the list of
joined_usersFor each user in the
joined_userslist:UserJoinedRoom (Code 16) : with room
name(+ stats) ofjoinerof the room
Send to
joiner:-
room: name of joined roomusernames: list of roomjoined_usersuser stats, online status, etc
owner:ownerifis_private=truefor the roomoperators:ownerifis_private=truefor the room
-
room: name of joined roomtickers: array of roomtickers
-
Function: Leave Room
Function to leave a room
Actors:
leaver: user requesting or being removed from the room
Actions:
Remove the user from the list of
joined_usersFor each user in the
joined_userslist:UserLeftRoom (Code 17) : with room
nameand list (+ stats) ofleaverof the room
Send to
leaver:LeaveRoom (Code 15) : with room
name
Function: Grant Membership to Private Room
Function to grant membership to a user from a private room
Actors:
granter: User granting membershipgrantee: User being granted membership
Actions:
Add new member to the
memberslistFor each member in the
memberslist:Send PrivateRoomGrantMembership (Code 134) with room name and new member name
For the
grantee:Send PrivateRoomMembershipGranted (Code 139) with the room name
If the
granteris in the list of roomoperators:Function: Notify room owner : “User [
grantee] was added as a member of room [name] by operator [granter]”
If the
granteris the roomowner:Function: Notify room owner : “User
granteris now a member of roomname”
Function: Revoke Membership from Private Room
Function to revoke membership from a user from a private room
Actors:
revokee: User having membership revoked
Actions:
Remove user from the
memberslistFor each member in the
memberslist:Send PrivateRoomRevokeMembership (Code 135) with room name and removed member name
For the room
owner:Function: Notify room owner : “User
revokeeis no longer a member of roomname”
For the
revokee:Send PrivateRoomMembershipRevoked (Code 140) with the room name
If the
revokeeis in thejoined_userslist:Function: Leave Room : for the
revokee
For the
revokee:
Note
No specialized message is sent to the owner if an operator removes a member unlike when adding a member
Function: Grant Operator to Private Room
Function to grant operator privileges to a member of a private room
Actors:
granter: User granting the operator privilegesgrantee: User having operator privileges granted
Actions:
Add the member to the
operatorslist:For each member in the
memberslist:Send PrivateRoomGrantOperator (Code 143) with room name and the name of the new operator
For each user in the
joined_userslist:Send PrivateRoomGrantOperator (Code 143) with room name and the name of the new operator
For the
grantee:Send PrivateRoomOperatorGranted (Code 145) with the room name
For the room
owner:Function: Notify room owner : “User
granteeis now an operator of roomname”
Note
It is not a mistake that the PrivateRoomGrantOperator (Code 143) message gets sent twice to both the joined users and the members
Function: Revoke Operator from Private Room
Function to revoke operator privileges from a member of a private room
Actors:
revoker: User revoking the operator privilegesrevokee: User having operator privileges revoked
Actions:
Remove the member from the
operatorslist:For each member in the
memberslist:Send PrivateRoomRevokeOperator (Code 144) with room name and the name of the removed operator
For each user in the
joined_userslist:Send PrivateRoomRevokeOperator (Code 144) with room name and the name of the removed operator
For the
revokee:Send PrivateRoomOperatorRevoked (Code 146) with the room name
For the room
owner:Function: Notify room owner : “User
revokeeis no longer an operator of roomname”
Note
It is not a mistake that the PrivateRoomRevokeOperator (Code 144) message gets sent twice to both the joined users and the members
Function: Server Notification Message
The server will send private chat messages to report errors and information back to the client:
Actors:
receiver: User receiving a server notification message
Actions:
Generate a
chat_idServer to
receiver:Send PrivateChatMessage (Code 22)
chat_id : Generated
chat_idtimestamp : Current timestamp
username :
servermessage =
messageis_direct = true
Client Flows
This section describes some of the flows from the client point of view. Specifically it focuses on the interactions between the client and the server
Private Chat Message
Actors:
sender: User sending a private messagereceiver: User receiving the private message
Actions:
senderto server:PrivateChatMessage (Code 22) (username =
receiver, message =message)
Server to
receiverPrivateChatMessage (Code 22) (username =
sender, chat_id =<generated>, message =message)
receiverto server:PrivateChatMessageAck (Code 23) (chat_id =
<chat_id from received message>)
Note
Some unresolved questions are:
Is a message still delivered after the sender is removed?
Searching
Query rules
Exclusion: dash-character gets used to exclude terms. Example:
-mp3, would exclude all mp3 filesWildcard: asterisk-character for wildcard searches. Example:
*oney, would match ‘honey’ and ‘money’Sentence matching: double quotes would get used to keep terms together. Example:
"my song"would perform an exact match for those terms. This no longer seems to be implemented.
Undescribed rules (matching):
Searches are case-insensitive
Placement of terms is irrelevant. This also applies to exclusions
-mp3 songis the same assong -mp3Wildcard/exclusion: placement is irrelevant
Wildcard: can only be used in the beginning of the word.
some*is not valid and neither issome*thingWildcard: doesn’t need to match a character. Query
*song.mp3will matchsong.mp3`Wildcard: query
song *will return somethingExclusion: there are results for queries using only exclusions but it does not seem official. Example
-mp3, returns a limited number of results and some results even containing stringmp3Path seperators can be included (backslash and forward slash) but only apply to the directory part of the filename. Query
my\coolwill match filemy\cool\band\song.mp3, but queryband\songwill not match the same file
The algorithm for matching can be described as:
Split the query into search terms using whitespace
Foreach term match the item’s path in the form of:
<non-word character or start of string>
when using wildcard: <0 or more word characters>
escaped search term
<non-word character or end of string>
Word characters are alphanumeric characters or unicode word characters
Attributes
Each search results returns a list of attributes containing information about the file.
Investigated different file formats and which attributes they return in which the following formats were checked: FLAC, MP3, M4A, OGG, AAC, WAV. It seems like there’s a categorization of the different formats, based on the category certain attributes will be returned:
Lossless: FLAC, WAV
Compressed: MP3, M4A, AAC, OGG
Attribute table:
Index |
Meaning |
Usage |
|---|---|---|
0 |
bitrate |
compressed |
1 |
length in seconds |
compressed, lossless |
2 |
VBR |
compressed |
4 |
sample rate |
lossless |
5 |
bitness |
lossless |
Note
The extension parameter is empty for anything but mp3 and flac
Note
Couldn’t find any other than these. Number 3 seems to be missing, could this be something used in the past or maybe for video? Theoretically we could invent new attributes here, like something for video, images, extra metadata for music files. The official clients don’t seem to do anything with the extra attributes