Documentation Index Fetch the complete documentation index at: https://mintlify.com/nicotine-plus/nicotine-plus/llms.txt
Use this file to discover all available pages before exploring further.
Learn from these practical plugin examples that demonstrate common use cases and implementation patterns.
Simple Chat Command
This example shows how to create custom commands using the modern command system:
# ~/.local/share/nicotine/plugins/hello_world/__init__.py
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .commands = {
"hello" : {
"aliases" : [ "hi" , "greet" ],
"description" : "Greet a user" ,
"parameters" : [ "[name]" ],
"callback" : self .hello_command
}
}
def hello_command ( self , args , ** kwargs ):
"""Handle the /hello command."""
name = args.strip() if args else "World"
self .output( f "Hello, { name } !" )
return True # Indicate success
# PLUGININFO
Version = "1.0.0"
Authors = [ "Your Name" ]
Name = "Hello World"
Description = "Simple greeting command example."
Commands automatically work in chat rooms, private chats, and CLI unless you use the disable parameter.
Advanced Command with Interface-Specific Behavior
This example shows interface-specific parameters and callbacks:
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .commands = {
"sample" : {
"aliases" : [ "demo" ],
"description" : "Sample command with interface variations" ,
"disable" : [ "cli" ], # Don't allow in CLI
"callback" : self .sample_command,
"callback_chatroom" : self .sample_chatroom_command,
"parameters" : [ "<choice1|choice2>" , "<message..>" ],
"parameters_chatroom" : [ "<room_choice1|room_choice2>" ]
}
}
def sample_command ( self , args , user = None , ** kwargs ):
"""Default handler for private chat."""
self .output( f "Private chat command: { args } " )
def sample_chatroom_command ( self , args , room = None , ** kwargs ):
"""Specific handler for chat rooms."""
self .output( f "Room command in { room } : { args } " )
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .commands = {
"sample" : {
"aliases" : [ "demo" ],
"description" : "Sample command description" ,
"disable" : [ "private_chat" ],
"callback" : self .sample_command,
"callback_private_chat" : self .sample_command,
"parameters" : [ "<choice1|choice2>" , "<something..>" ],
"parameters_chatroom" : [ "<choice55|choice2>" ]
}
}
def sample_command ( self , _args , ** _unused ):
self .output( "Hello" )
Anti-Shout: Text Transformation Plugin
Converts ALL CAPS messages to proper capitalization:
# anti_shout/__init__.py
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .settings = {
"maxscore" : 0.6 ,
"minlength" : 10
}
self .metasettings = {
"maxscore" : {
"description" : "The maximum ratio of capitals before converting" ,
"type" : "float" ,
"minimum" : 0 ,
"maximum" : 1 ,
"stepsize" : 0.1
},
"minlength" : {
"description" : "Lines shorter than this will be ignored" ,
"type" : "integer" ,
"minimum" : 0
}
}
@ staticmethod
def capitalize ( text ):
# Don't alter protocol links (http://, ftp://)
if text.find( "://" ) > - 1 :
return text
return text.capitalize()
def incoming_private_chat_event ( self , user , line ):
return user, self .antishout(line)
def incoming_public_chat_event ( self , room , user , line ):
return room, user, self .antishout(line)
def antishout ( self , line ):
lowers = len ([x for x in line if x.islower()])
uppers = len ([x for x in line if x.isupper()])
score = - 2 # Unknown state (no letters at all)
if uppers > 0 :
score = - 1 # At least some upper letters
if lowers > 0 :
score = uppers / float (lowers)
# Check if message exceeds thresholds
if ( len (line) > self .settings[ "minlength" ] and
(score == - 1 or score > self .settings[ "maxscore" ])):
newline = ". " .join([ self .capitalize(x) for x in line.split( ". " )])
return newline + " [as]" # Mark as anti-shouted
return line
This plugin uses events to modify messages in real-time. Keep the logic fast to avoid UI freezing.
Auto-Responder with Throttling
Responds to trigger words while avoiding chat flood bans:
# testreplier/__init__.py
from random import choice
from pynicotine.pluginsystem import BasePlugin, ResponseThrottle
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .settings = {
"replies" : [ "Test successful!" , "Working!" , "All systems go!" ]
}
self .metasettings = {
"replies" : {
"description" : "Replies:" ,
"type" : "list string"
}
}
# Initialize throttle to prevent spam/ban
self .throttle = ResponseThrottle(
self .core,
self .human_name,
logging = True # Log rejected responses
)
def incoming_public_chat_event ( self , room , user , line ):
# Only respond to "test" trigger
if line.lower() != "test" :
return
# Check if safe to respond (respects timing limits)
if self .throttle.ok_to_respond(room, user, line, seconds_limit_min = 30 ):
self .throttle.responded() # Record this response
# Send random reply from settings
reply = choice( self .settings[ "replies" ]).lstrip( "!" )
self .send_public(room, reply)
ResponseThrottle prevents:
Same user requesting same thing too quickly (12× limit)
Different users requesting same thing too quickly (3× limit)
Responding in the same room too recently
Responding in too many rooms simultaneously
Auto-Buddy Room Members
Automatically adds private room members as buddies:
# auto_buddy_rooms/__init__.py
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .settings = {
"watch_all_rooms" : True ,
"prioritize_buddies" : False ,
"rooms" : []
}
self .metasettings = {
"watch_all_rooms" : {
"description" : "Watch every joined private room" ,
"type" : "bool"
},
"prioritize_buddies" : {
"description" : "Give buddies priority status" ,
"type" : "bool"
},
"rooms" : {
"description" : "Watched private rooms:" ,
"type" : "list string"
}
}
def private_room_member_added_notification ( self , room , user ):
# Don't add yourself
if user == self .core.users.login_username:
return
# Check if we're watching this room
if not self .settings[ "watch_all_rooms" ]:
if room not in self .settings[ "rooms" ]:
return
# Add as buddy
self .core.buddies.add_buddy(user)
# Set note if buddy doesn't have one
if not self .core.buddies.users[user].note:
self .core.buddies.set_buddy_note(
user,
f "Auto-Buddy member in private room { room } "
)
# Optionally prioritize
if self .settings[ "prioritize_buddies" ]:
self .core.buddies.set_buddy_prioritized(user, True )
Enable the plugin
Go to Preferences → Plugins and enable “Auto-Buddy Room Members”
Configure settings
Choose whether to watch all rooms or specify specific rooms to monitor
Join private rooms
When new members are added, they’ll automatically become buddies
Demonstrates different setting UI types:
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
"""Radio Button/Dropdown Example."""
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .settings = {
"player_radio" : 2 , # ID (starts from 0)
"player_dropdown" : "Clementine" # String or ID
}
self .metasettings = {
"player_radio" : {
"description" : "Choose an audio player" ,
"type" : "radio" ,
"options" : (
"Exaile" ,
"Audacious" ,
"Clementine"
)
},
"player_dropdown" : {
"description" : "Choose an audio player" ,
"type" : "dropdown" ,
"options" : (
"Exaile" ,
"Audacious" ,
"Clementine"
)
}
}
def init ( self ):
# Access the selected value
selected_radio = self .settings[ "player_radio" ] # Returns: 2
selected_dropdown = self .settings[ "player_dropdown" ] # Returns: "Clementine"
Search Monitoring Plugin
Log and respond to search requests:
from pynicotine.pluginsystem import BasePlugin
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .settings = {
"log_searches" : True ,
"notify_popular" : True ,
"popular_threshold" : 5
}
self .metasettings = {
"log_searches" : {
"description" : "Log all search requests" ,
"type" : "bool"
},
"notify_popular" : {
"description" : "Notify when search term is popular" ,
"type" : "bool"
},
"popular_threshold" : {
"description" : "Number of searches to be considered popular" ,
"type" : "integer" ,
"minimum" : 1
}
}
self .search_counts = {}
def search_request_notification ( self , searchterm , user , token ):
if self .settings[ "log_searches" ]:
self .log( f " { user } searched for: { searchterm } " )
# Track popular searches
if self .settings[ "notify_popular" ]:
self .search_counts[searchterm] = self .search_counts.get(searchterm, 0 ) + 1
if self .search_counts[searchterm] == self .settings[ "popular_threshold" ]:
self .log( f "Popular search detected: ' { searchterm } ' "
f "( { self .search_counts[searchterm] } requests)" )
Download/Upload Tracker
Track transfer statistics:
from pynicotine.pluginsystem import BasePlugin
import time
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .transfers = {
"uploads" : [],
"downloads" : []
}
def upload_started_notification ( self , user , virtual_path , real_path ):
self .transfers[ "uploads" ].append({
"user" : user,
"file" : virtual_path,
"started" : time.time()
})
self .log( f "Upload started: { virtual_path } to { user } " )
def upload_finished_notification ( self , user , virtual_path , real_path ):
# Find the matching upload
for upload in self .transfers[ "uploads" ]:
if upload[ "file" ] == virtual_path and upload[ "user" ] == user:
duration = time.time() - upload[ "started" ]
self .log( f "Upload completed in { duration :.1f} s: { virtual_path } " )
break
def download_finished_notification ( self , user , virtual_path , real_path ):
import os
file_size = os.path.getsize(real_path)
self .log( f "Downloaded { file_size / 1024 / 1024 :.2f} MB from { user } " )
Connection Monitor
Track server connection status:
from pynicotine.pluginsystem import BasePlugin
import time
class Plugin ( BasePlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .connect_time = None
def server_connect_notification ( self ):
self .connect_time = time.time()
self .log( "Connected to Soulseek server" )
def server_disconnect_notification ( self , userchoice ):
if self .connect_time:
uptime = time.time() - self .connect_time
hours = uptime / 3600
reason = "User disconnected" if userchoice else "Connection lost"
self .log( f " { reason } . Uptime was { hours :.2f} hours" )
self .connect_time = None
Best Practices
Prefer Notifications Over Events
Use notifications (_notification) unless you need to modify data. Events can freeze the UI if slow.
Use ResponseThrottle for Auto-Responders
Always throttle automatic chat responses to avoid server bans.
Provide User Settings
Make plugins configurable through settings and metasettings dictionaries.
Clean Up Resources
Implement disable() to clean up timers, connections, and resources.
Log Important Actions
Use self.log() for debugging and user feedback.
Additional Resources
Getting Started Learn the basics of plugin development
API Reference Complete method and event reference
Official Examples Browse example plugins in the repository
Source Code View pluginsystem.py source code