Shift + Delete فایل را حذف کرد، اما این فرمان روی آیتمی اعمال میشود که در Playlist انتخاب شده است. یعنی اگر هنگام کار در مرورگر، Explorer، ویرایشگر متن یا هر برنامه دیگری باشم، ابتدا باید به AIMP برگردم، آهنگ را پیدا یا Select کنم و بعد آن را حذف کنم.برای آرشیوی با این حجم، همین چند کلیک اضافه خیلی زود آزاردهنده میشود. در این پروژه کوچک یک اسکریپت AutoHotkey v2 ساختم که با فشردن یک میانبر سراسری:Ctrl + Delete
آهنگ در حال پخش AIMP را تشخیص میدهد، AIMP را به آهنگ بعدی میبرد و فایل قبلی را مستقیماً به Recycle Bin ویندوز منتقل میکند؛ حتی اگر در همان لحظه داخل مرورگر، File Explorer یا هر نرمافزار دیگری باشم.
اگر هنوز با خود AutoHotkey، تفاوت نسخه 1 و 2، تعریف Hotkeyها یا کاربردهای عمومی آن در ویندوز آشنا نیستید، مقاله برنامه AutoHotkey، بهترین برنامه اتوماسیون رایگان ویندوز مرجع اصلی DJH برای آن مباحث است. این مقاله عمداً آن آموزش پایه را تکرار نمیکند و روی معماری، کد، تست و عیبیابی همین پروژه AIMP تمرکز دارد.
مسئله اصلی چه بود؟
فرض کنید در حال گوشدادن به چند هزار آهنگ هستید و همزمان چند کار دیگر انجام میدهید و پونصد تا پنجره و برنامه رو مانیتورها بازه. یکی از آهنگها را نمیپسندید و میخواهید همان لحظه حذفش کنید.
روش عادی تقریباً چنین مسیری دارد:
- رفتن به پنجره AIMP
- پیدا کردن Track در Playlist
- اطمینان از اینکه همان Track انتخاب شده است
- فشردن فرمان حذف
- برگشتن به برنامهای که با آن کار میکردم
اگر این کار را چند بار انجام دهید مسئله مهمی نیست، اما برای هرس یک آرشیو موسیقی بزرگ، این Workflow بسیار کند میشود مخصوصا اینکه موزیک ها در هم و از کانال های مختلف یوتیوب و تلگرام دانلود شده باشند و اکثرا باب سلیقه شمانباشند.
چیزی که میخواستم سادهتر بود:
آهنگ بد است یا حال نمیکنم باهاش؟
↓
Ctrl + Delete
↓
تمام
یعنی Playlist، فوکوس پنجره و محل قرارگیری Track اهمیتی نداشته باشد.
هدف پروژه
هدف نهایی این اسکریپت چند Requirement مشخص داشت:
- حذف Current Playing Track، نه Track انتخابشده در Playlist
- کارکردن Hotkey بهصورت Global در کل ویندوز
- استفاده از
Ctrl + Delete - عدم نیاز به فعالبودن پنجره AIMP
- عدم نیاز به Plugin جانبی برای AIMP
- عدم نیاز به فایل واسط برای ذخیره نام آهنگ
- رفتن به Track بعدی قبل از حذف فایل قبلی
- حذف امن از طریق Recycle Bin
- جلوگیری از حذف فایل در صورتی که مسیر Track با اطمینان تشخیص داده نشود
- داشتن Log برای عیبیابی
در نتیجه این پروژه عملاً فقط یک Hotkey ساده نیست؛ بخش مهم آن این است که قبل از اجرای عملیات فایل، مطمئن شویم همان فایلی را حذف میکنیم که واقعاً در AIMP در حال پخش بوده است.
AIMP چیست؟
AIMP یک پخشکننده موسیقی برای ویندوز است که علاوه بر پخش فایلهای صوتی، امکاناتی مانند Playlist، مدیریت Library، Queue، تنظیمات خروجی صدا، Hotkey و قابلیتهای توسعهپذیر را در اختیار کاربر قرار میدهد. در این پروژه AIMP فقط یک Media Player ساده نیست؛ برنامهای است که باید بتوانیم بدون وابستگی به ظاهر پنجره و وضعیت انتخاب Playlist، بفهمیم دقیقاً کدام فایل در همان لحظه در حال پخش است.

این تفاوت مهم است. چیزی که روی صفحه در Playlist انتخاب شده، لزوماً همان Current Playing Track نیست. اگر Automation صرفاً روی Selection رابط کاربری تکیه کند، ممکن است فرمان روی فایل دیگری اجرا شود. به همین دلیل در این پروژه بهجای شبیهسازی کلیک روی Playlist، اطلاعات Track جاری مستقیماً از Remote Information موجود در Shared Memory خوانده میشود.
AIMP همچنین میتواند فایل محلی، Playlist و منابع دیگری را مدیریت کند، اما Script این مقاله عمداً فقط زمانی اجازه حذف میدهد که Track جاری به یک Local File واقعی روی Disk اشاره کند. Stream، مسیر نامعتبر یا دادهای که نتوان آن را با اطمینان تأیید کرد، وارد مرحله Recycle نمیشود.
بنابراین قبل از ورود به کد کافی است این مدل ذهنی را داشته باشید: AIMP منبع وضعیت Playback است و AutoHotkey وضعیت موردنیاز را از آن میخواند، فرمان Next را ارسال میکند و عملیات فایل را در Windows انجام میدهد.
AutoHotkey چیست؟
AutoHotkey یک ابزار اسکریپتنویسی و اتوماسیون برای Windows است که میتواند Hotkeyهای سراسری بسازد، ورودی Keyboard و Mouse را مدیریت کند، با Windowها و Processها ارتباط داشته باشد، فایلها را پردازش کند و از طریق DllCall مستقیماً Windows API را فراخوانی کند. همین ترکیب قابلیتها باعث شد برای این پروژه انتخاب مناسبی باشد.

در سادهترین استفاده، AutoHotkey میتواند با چند خط کد یک میانبر مثل Ctrl + Delete تعریف کند؛ اما در این پروژه از سطح ساده Hotkey فراتر میرویم. Script باید Shared Memory را باز کند، داده Binary را از Offsetهای مشخص بخواند، رشته UTF-16 را استخراج کند، Process و File Path را اعتبارسنجی کند، Windows Message بفرستد و در پایان فایل را به Recycle Bin منتقل کند.
کد این مقاله مخصوص AutoHotkey v2 است. Syntax نسخه 2 با AutoHotkey v1 تفاوتهای مهمی دارد؛ بنابراین اگر تازه با این ابزار آشنا شدهاید یا میخواهید نصب، ساخت فایل .ahk، تعریف Hotkeyها و کاربردهای عمومی آن را جداگانه یاد بگیرید، ابتدا مقاله برنامه AutoHotkey، بهترین برنامه اتوماسیون رایگان ویندوز را ببینید.
برای Automationهای مدیریتیتر Windows نیز آموزش PowerShell از صفر تا صد مکمل خوبی است. تفاوت این پروژه در این است که AutoHotkey برای Global Hotkey، تعامل سریع با Desktop و Windows API در یک Script کوچک، Workflow بسیار مستقیمی ایجاد میکند.
قبل از ورود به کد
برای دنبالکردن ادامه مقاله لازم نیست از قبل متخصص Windows API یا Shared Memory باشید. کافی است نقش اجزای محیط اجرا را از هم جدا کنیم: AIMP آهنگ را پخش میکند و وضعیت Track جاری را در اختیار Remote Interface قرار میدهد؛ AutoHotkey Script همیشه در پسزمینه اجرا میماند؛ Windows API پل دسترسی به Shared Memory و ارسال Media Command را فراهم میکند؛ و Recycle Bin آخرین لایه ایمنی عملیات فایل است.
| جزء | نقش در پروژه |
|---|---|
| AIMP | پخش Track و ارائه اطلاعات Track جاری |
| AutoHotkey v2 | Hotkey، منطق Script، Validation و FileRecycle |
| Windows API | خواندن Memory Mapping و ارسال فرمان Media |
| Recycle Bin | حذف قابل بازیابی بهجای Permanent Delete |
از این نقطه به بعد، کد را بر اساس همین مسیر بررسی میکنیم: Hotkey → تشخیص AIMP → خواندن Current Track → Validation → Next → Verification → Recycle → Log. به این ترتیب هر بخش کد دقیقاً در جایگاه عملی خودش قابل فهم خواهد بود.
محیط تست پروژه
نسخه نهایی این پروژه روی محیط واقعی ویندوز و هنگام پخش موسیقی با AIMP تست شد.
اجزای اصلی عبارتاند از:
| بخشاستفاده | |
|---|---|
| سیستمعامل | Windows 11 |
| Music Player | AIMP |
| زبان اسکریپت | AutoHotkey v2 |
| Hotkey | Ctrl + Delete |
| روش حذف | Windows Recycle Bin |
| Plugin برای AIMP | نیاز ندارد |
| فایل واسط | نیاز ندارد |
| اینترنت | نیاز ندارد |
نسخه دقیق Build مربوط به AIMP و AutoHotkey در این مرحله ثبت نشده است، بنابراین ترجیح میدهم عددی را حدس نزنم. چیزی که در تست واقعی تأیید شد این بود که نسخه نهایی اسکریپت، یعنی v1.1.0، در محیط مورد استفاده من بهدرستی کار میکند.
معماری راهحل
در ابتدا میشد از یک Plugin برای AIMP استفاده کرد که اطلاعات Current Track را داخل فایل متنی بنویسد و AutoHotkey آن فایل را بخواند.

این روش ساده بود، اما یک مشکل داشت: برای یک کار بسیار کوچک، وابستگی اضافه ایجاد میکرد.
ساختار آن چیزی شبیه این بود:
AIMP
↓
Plugin
↓
current-track.txt
↓
AutoHotkey
↓
Delete
من ترجیح دادم ارتباط مستقیم باشد:
AIMP
↓
Shared Memory
↓
AutoHotkey
↓
Next Track
↓
Recycle Bin
بنابراین نسخه نهایی مستقیماً اطلاعات Track فعلی را از Shared Memory مربوط به AIMP دریافت میکند.
نسخه اول و مشکلی که داشت
اولین نسخه مستقیم با شماره v1.0.0 ساخته شد، اما در تست واقعی کار نکرد.
بهجای ادامهدادن با حدس، نسخه دوم را Diagnostic طراحی کردم تا مراحل مختلف را Log کند:
- آیا
AIMP.exeدر حال اجراست؟ - آیا Shared Memory پیدا میشود؟
- آیا مسیر Current Track خوانده شده؟
- آیا Track واقعاً یک فایل محلی است؟
- آیا فرمان Next ارسال شده؟
- آیا AIMP از Track قبلی خارج شده؟
- آیا فایل وارد Recycle Bin شده؟
این تغییر باعث شد نسخه v1.1.0 هم قابل عیبیابیتر باشد و هم در تست واقعی درست کار کند.
کد کامل
نسخهای که در تست نهایی استفاده شد:
#Requires AutoHotkey v2.0
#SingleInstance Force
Persistent
; ============================================================
; DJH AIMP - Delete Current Playing Track
; Version : 1.1.0
; Hotkey : Ctrl + Delete
;
; Direct / plugin-free:
; 1) Reads current track from AIMP shared memory
; 2) Switches AIMP to Next
; 3) Moves previous file to Recycle Bin
;
; Safety:
; - Never permanently deletes
; - Never deletes unless a valid local file path is read
; - Writes a small diagnostic log beside this script
; ============================================================
global FILE_MAP_READ := 0x0004
global MAP_SIZE := 2048
global Busy := false
global LogFile := A_ScriptDir "\DJH-AIMP-Delete.log"
^Delete::
{
global Busy
if Busy
return
Busy := true
try
DeleteCurrentTrack()
finally
SetTimer(ReleaseLock, -600)
}
ReleaseLock()
{
global Busy
Busy := false
}
DeleteCurrentTrack()
{
Log("HOTKEY | Ctrl+Delete")
if !ProcessExist("AIMP.exe")
{
Fail("AIMP.exe is not running")
return
}
TrackPath := GetCurrentAIMPFile()
if (TrackPath = "")
{
Fail("Could not read current track from AIMP")
return
}
Log("TRACK | " TrackPath)
Attr := FileExist(TrackPath)
if (Attr = "")
{
Fail("Track path is not a local file")
return
}
if InStr(Attr, "D")
{
Fail("Track path points to a directory")
return
}
FileToDelete := TrackPath
SplitPath(FileToDelete, &FileName)
; First try sending Next directly to an AIMP process window.
NextOK := SendNextToAIMP()
if !NextOK
{
Log("NEXT | direct message failed -> media-key fallback")
try
{
Send("{Media_Next}")
NextOK := true
}
catch
NextOK := false
}
if !NextOK
{
Fail("Could not switch AIMP to next track")
return
}
Log("NEXT | sent")
; Allow decoder / file handle to move away from previous track.
Sleep(700)
; Confirm AIMP has actually moved away when possible.
NewTrack := GetCurrentAIMPFile()
if (NewTrack != "")
{
Log("AFTER NEXT | " NewTrack)
if PathsEqual(NewTrack, FileToDelete)
{
; One additional media-key attempt.
Log("NEXT | track unchanged -> retry")
try Send("{Media_Next}")
Sleep(900)
NewTrack := GetCurrentAIMPFile()
if (NewTrack != "" && PathsEqual(NewTrack, FileToDelete))
{
Fail("AIMP did not leave the current track; nothing deleted")
return
}
}
}
; Recycle with short retries for delayed file-handle release.
for Delay in [0, 300, 700, 1200]
{
if (Delay > 0)
Sleep(Delay)
try
{
FileRecycle(FileToDelete)
Log("RECYCLE | PASS | " FileToDelete)
Notice("Recycled: " FileName)
return
}
catch as Err
{
Log("RECYCLE | retry | " Err.Message)
}
}
Fail("Could not move file to Recycle Bin")
}
GetCurrentAIMPFile()
{
global FILE_MAP_READ, MAP_SIZE
; AIMP installations / Windows sessions can expose the mapping
; with or without a Local\ namespace prefix.
for MapName in ["AIMP2_RemoteInfo", "Local\AIMP2_RemoteInfo"]
{
Result := ReadAIMPMapping(MapName)
if (Result != "")
{
Log("MAPPING | PASS | " MapName)
return Result
}
}
Log("MAPPING | FAIL")
return ""
}
ReadAIMPMapping(MapName)
{
global FILE_MAP_READ, MAP_SIZE
hMap := DllCall(
"Kernel32\OpenFileMappingW",
"UInt", FILE_MAP_READ,
"Int", 0,
"WStr", MapName,
"Ptr"
)
if !hMap
return ""
pView := DllCall(
"Kernel32\MapViewOfFile",
"Ptr", hMap,
"UInt", FILE_MAP_READ,
"UInt", 0,
"UInt", 0,
"UPtr", MAP_SIZE,
"Ptr"
)
if !pView
{
DllCall("Kernel32\CloseHandle", "Ptr", hMap)
return ""
}
Result := ""
try
{
; Packed AIMP2FileInfo structure:
; DWORD cbSizeOf @ 0
; BOOL nActive @ 4
; DWORD nBitRate @ 8
; DWORD nChannels @ 12
; DWORD nDuration @ 16
; INT64 nFileSize @ 20
; DWORD nRating @ 28
; DWORD nSampleRate @ 32
; DWORD nTrackID @ 36
; DWORD nAlbumLen @ 40
; DWORD nArtistLen @ 44
; DWORD nDateLen @ 48
; DWORD nFileNameLen @ 52
; DWORD nGenreLen @ 56
; DWORD nTitleLen @ 60
;
; UTF-16 strings follow immediately after cbSizeOf bytes.
StructSize := NumGet(pView, 0, "UInt")
Active := NumGet(pView, 4, "Int")
AlbumLen := NumGet(pView, 40, "UInt")
ArtistLen := NumGet(pView, 44, "UInt")
DateLen := NumGet(pView, 48, "UInt")
FileNameLen := NumGet(pView, 52, "UInt")
Log("REMOTE | size=" StructSize
" active=" Active
" album=" AlbumLen
" artist=" ArtistLen
" date=" DateLen
" filename=" FileNameLen)
if (StructSize < 64 || StructSize > 512)
return ""
if (FileNameLen < 1 || FileNameLen > 800)
return ""
PrefixChars := AlbumLen + ArtistLen + DateLen
FileOffset := StructSize + PrefixChars * 2
FileBytes := FileNameLen * 2
if (FileOffset < StructSize)
return ""
if (FileOffset + FileBytes > MAP_SIZE)
return ""
FilePtr := pView + FileOffset
Result := StrGet(FilePtr, FileNameLen, "UTF-16")
Result := Trim(StrReplace(Result, Chr(0)), " `t`r`n")
}
catch as Err
{
Log("REMOTE | exception | " Err.Message)
Result := ""
}
finally
{
DllCall("Kernel32\UnmapViewOfFile", "Ptr", pView)
DllCall("Kernel32\CloseHandle", "Ptr", hMap)
}
return Result
}
SendNextToAIMP()
{
; WM_APPCOMMAND is sent to a real top-level AIMP window, not the
; hidden shared-memory window used in v1.0.0.
static WM_APPCOMMAND := 0x0319
static APPCOMMAND_MEDIA_NEXTTRACK := 11
DetectHiddenWindows(true)
hwnd := 0
; Prefer a visible/top-level AIMP process window.
try hwnd := WinExist("ahk_exe AIMP.exe")
if !hwnd
{
DetectHiddenWindows(false)
return false
}
lParam := APPCOMMAND_MEDIA_NEXTTRACK << 16
try
{
SendMessage(
WM_APPCOMMAND,
hwnd,
lParam,
,
"ahk_id " hwnd
)
DetectHiddenWindows(false)
return true
}
catch as Err
{
Log("NEXT | SendMessage exception | " Err.Message)
DetectHiddenWindows(false)
return false
}
}
PathsEqual(A, B)
{
return StrLower(Trim(A)) = StrLower(Trim(B))
}
Notice(Text)
{
ToolTip(Text)
SetTimer(() => ToolTip(), -2200)
}
Fail(Text)
{
Log("FAIL | " Text)
SoundBeep(800, 120)
ToolTip("AIMP Delete: " Text)
SetTimer(() => ToolTip(), -3500)
}
Log(Text)
{
global LogFile
try
{
Stamp := FormatTime(, "yyyy-MM-dd HH:mm:ss")
FileAppend("[" Stamp "] " Text "`r`n", LogFile, "UTF-8")
}
}
Hotkey چگونه تعریف شده؟
مهمترین خط برای Trigger کردن برنامه این است:
^Delete::
در AutoHotkey علامت ^ معرف کلید Ctrl است.
بنابراین:
^Delete
معادل است با:
Ctrl + Delete
این Hotkey به یک پنجره خاص محدود نشده است، بنابراین تا زمانی که اسکریپت در حال اجرا باشد میتوان آن را از برنامههای مختلف فراخوانی کرد.
جلوگیری از اجرای دوباره
اولین نکته امنیتی این اسکریپت متغیر زیر است:
global Busy := false
بعد هنگام اجرای Hotkey:
if Busy
return
Busy := true
اگر کاربر ناخواسته چند بار پشت سر هم Ctrl + Delete بزند، اجرای قبلی هنوز در حال پردازش است و درخواست جدید نادیده گرفته میشود.
پس از پایان عملیات:
SetTimer(ReleaseLock, -600)
قفل کمی بعد آزاد میشود.
این جزئیات برای اسکریپتی که قرار است فایل واقعی حذف کند اهمیت زیادی دارد.
بررسی اجرای AIMP
قبل از هر کاری این قسمت اجرا میشود:
if !ProcessExist("AIMP.exe")
{
Fail("AIMP.exe is not running")
return
}
اگر AIMP باز نباشد، اسکریپت هیچ عملیات دیگری انجام نمیدهد.
بهجای اینکه فرض کند Player وجود دارد، ابتدا Process را بررسی میکند.
پیدا کردن آهنگ در حال پخش
هسته اصلی پروژه تابع زیر است:
TrackPath := GetCurrentAIMPFile()
هدف این تابع گرفتن Full Path فایل واقعی Track فعلی است.
مثلاً:
D:\Music\Rock\Example Song.flac
بهجای Title یا نامی که روی رابط AIMP نمایش داده میشود، ما به مسیر واقعی فایل نیاز داریم؛ چون در نهایت قرار است Windows همان فایل را به Recycle Bin انتقال دهد.
Shared Memory چیست؟
AIMP بخشی از اطلاعات وضعیت فعلی خود را در یک Memory Mapping در اختیار برنامههای دیگر قرار میدهد.
اسکریپت دو نام را امتحان میکند:
"AIMP2_RemoteInfo"
و:
"Local\AIMP2_RemoteInfo"
کد:
for MapName in ["AIMP2_RemoteInfo", "Local\AIMP2_RemoteInfo"]
{
Result := ReadAIMPMapping(MapName)
if (Result != "")
return Result
}
این موضوع یکی از تفاوتهای مهم نسخه اصلاحشده با اولین نسخه بود.
باز کردن Shared Memory
برای دسترسی به Memory Mapping از Windows API استفاده شده است:
DllCall(
"Kernel32\OpenFileMappingW",
...
)
بعد Shared Memory داخل فضای Process اسکریپت Map میشود:
DllCall(
"Kernel32\MapViewOfFile",
...
)
در این مرحله AutoHotkey دیگر صرفاً در سطح ارسال کلید نیست؛ مستقیماً از APIهای ویندوز برای خواندن اطلاعات AIMP استفاده میکند.
استخراج FileName
اطلاعات Shared Memory شامل فیلدهای مختلف Track است.
از جمله طول:
- Album
- Artist
- Date
- FileName
- Genre
- Title
قسمتی که برای پروژه ما مهم است:
FileNameLen := NumGet(pView, 52, "UInt")
سپس Offset مربوط به FileName محاسبه میشود:
PrefixChars := AlbumLen + ArtistLen + DateLen
FileOffset := StructSize + PrefixChars * 2
دلیل ضرب در ۲ این است که رشتهها بهصورت UTF-16 خوانده میشوند.
در نهایت:
Result := StrGet(FilePtr, FileNameLen, "UTF-16")
مسیر فایل استخراج میشود.
چرا Validation اهمیت دارد؟
قرار نیست هر چیزی که از حافظه خوانده شد مستقیماً حذف شود.
مثلاً این بررسی انجام میشود:
if (StructSize < 64 || StructSize > 512)
return ""
و:
if (FileNameLen < 1 || FileNameLen > 800)
return ""
همچنین بررسی میشود که آدرس محاسبهشده از محدوده Memory Mapping بیرون نزند:
if (FileOffset + FileBytes > MAP_SIZE)
return ""
هدف ساده است:
اگر داده مشکوک است، هیچ فایلی حذف نشود.
این اصل در چنین اسکریپتی از کوتاهتر بودن کد مهمتر است.
بررسی فایل واقعی
بعد از استخراج Track:
Attr := FileExist(TrackPath)
اگر فایل وجود نداشته باشد:
if (Attr = "")
{
Fail("Track path is not a local file")
return
}
اگر مسیر به Directory اشاره کند:
if InStr(Attr, "D")
{
Fail("Track path points to a directory")
return
}
باز هم عملیات متوقف میشود.
در نتیجه اسکریپت فقط زمانی سراغ حذف میرود که یک Local File واقعی شناسایی شده باشد.
آیا واقعاً Track عوض شده؟
صرف ارسال Next کافی نیست.
بعد از آن:
Sleep(700)
و Current Track دوباره خوانده میشود:
NewTrack := GetCurrentAIMPFile()
سپس دو Path مقایسه میشوند:
if PathsEqual(NewTrack, FileToDelete)
اگر هنوز همان فایل در حال پخش باشد، اسکریپت دوباره تلاش میکند:
try Send("{Media_Next}")
Sleep(900)
اگر باز هم Track تغییر نکرد:
Fail("AIMP did not leave the current track; nothing deleted")
return
قسمت مهم پیام همین است:
nothing deleted
یعنی در شرایط نامطمئن، اولویت با حفظ فایل است.
حذف واقعی فایل
بعد از عبور از Validationها، فایل با این فرمان حذف میشود:
FileRecycle(FileToDelete)
نکته مهم این است که از FileDelete استفاده نکردهام.
FileRecycle فایل را به Recycle Bin میفرستد.
در نتیجه اگر اشتباهی Ctrl + Delete را فشار دهم، هنوز امکان Recover کردن آهنگ وجود دارد.
برای پروژهای که روی آرشیو بزرگی از موسیقی اجرا میشود، این لایه امنیتی ارزش زیادی دارد.
Retry برای فایلهای قفلشده
گاهی ممکن است Decoder بلافاصله پس از Next، Handle فایل را آزاد نکند.
برای همین چهار تلاش تعریف شده است:
for Delay in [0, 300, 700, 1200]
یعنی:
Attempt 1 → فوری
Attempt 2 → 300 ms
Attempt 3 → 700 ms
Attempt 4 → 1200 ms
اگر یکی موفق شود:
FileRecycle(FileToDelete)
و عملیات تمام میشود.
اگر هیچکدام موفق نشود، فایل دستنخورده باقی میماند و Error نمایش داده میشود.
خروجی روی صفحه
اگر عملیات موفق باشد یک Tooltip کوتاه نمایش داده میشود:
Recycled: Song Name.flac
تابع آن:
Notice(Text)
{
ToolTip(Text)
SetTimer(() => ToolTip(), -2200)
}
بعد از حدود دو ثانیه Tooltip خودکار بسته میشود.
خروجی خطا
اگر مشکلی رخ دهد:
Fail(Text)
اجرا میشود.
این تابع:
- خطا را در Log ثبت میکند.
- یک Beep کوتاه پخش میکند.
- Tooltip خطا را نمایش میدهد.
مثلاً:
AIMP Delete: Could not read current track from AIMP
این موضوع مهم است چون ممکن است هنگام فشردن Hotkey حتی پنجره AIMP را نبینیم.
فایل Log
نسخه نهایی یک فایل Log کنار اسکریپت ایجاد میکند:
DJH-AIMP-Delete.log
مسیر آن:
A_ScriptDir "\DJH-AIMP-Delete.log"
یعنی هرجا فایل .ahk قرار گرفته باشد، Log نیز همانجا ایجاد میشود.
یک اجرای موفق میتواند از نظر ساختاری چیزی شبیه این باشد:
[2026-08-24 16:30:00] HOTKEY | Ctrl+Delete
[2026-08-24 16:30:00] REMOTE | size=...
[2026-08-24 16:30:00] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:30:00] TRACK | D:\Music\Example.flac
[2026-08-24 16:30:00] NEXT | sent
[2026-08-24 16:30:01] AFTER NEXT | D:\Music\Next Song.flac
[2026-08-24 16:30:01] RECYCLE | PASS | D:\Music\Example.flac
این نمونه برای توضیح ساختار Log است؛ مقادیر واقعی هر سیستم و Track متفاوت خواهند بود.
اگر AIMP بسته باشد
در این حالت هیچ اتفاق خطرناکی نمیافتد.
Log:
HOTKEY | Ctrl+Delete
FAIL | AIMP.exe is not running
و اسکریپت خارج میشود.
اگر آهنگ Local File نباشد
فرض کنید Source فعلی یک Stream باشد یا AIMP مسیری ارائه کند که روی File System وجود ندارد.
این قسمت:
if (Attr = "")
باعث Abort میشود.
پس اسکریپت قرار نیست URL، Stream یا ورودی ناشناختهای را بهعنوان فایل حذف کند.
روش استفاده
پس از نصب AutoHotkey v2، فایل اسکریپت را اجرا میکنیم.
بعد AIMP را باز کرده و موسیقی را پخش میکنیم.
از اینجا به بعد فرقی نمیکند در چه برنامهای باشیم.
مثلاً:
Chrome
Visual Studio Code
File Explorer
Word
Photoshop
Desktop
هر زمان Track فعلی را نخواستیم:
Ctrl + Delete
نتیجه:
Current Song
↓
Ctrl + Delete
↓
AIMP → Next Song
↓
Old Song → Recycle Bin
و کار ادامه پیدا میکند.
تجربه استفاده واقعی
مهمترین مزیت این اسکریپت برای من خود کد نیست؛ تغییر Workflow است.
وقتی قرار است تعداد زیادی Track بررسی شوند، حذف چند مرحله کوچک از فرآیند تأثیر زیادی دارد.
روش قبلی:
شنیدن آهنگ
→ تصمیم به حذف
→ رفتن به AIMP
→ پیدا کردن Track
→ Select
→ Delete
→ برگشتن به کار
روش جدید:
شنیدن آهنگ
→ تصمیم به حذف
→ Ctrl + Delete
بهخصوص وقتی در حال هرس مجموعهای نزدیک به ۱۰۰ گیگابایت موسیقی هستم، این تفاوت کاملاً محسوس است.
چرا Permanent Delete نکردم؟
از نظر فنی میشد فایل را بهصورت مستقیم و غیرقابلبازیابی حذف کرد.
اما این کار را عمداً انجام ندادم.
وقتی Hotkey سراسری است، امکان فشردن تصادفی آن همیشه وجود دارد.
بنابراین:
FileRecycle
برای من انتخاب منطقیتری از حذف دائمی است.
Recycle Bin در این پروژه عملاً یک Safety Net محسوب میشود.
اگر در پایان دوره هرس مطمئن باشم فایلها را نمیخواهم، میتوانم Recycle Bin را بهصورت جداگانه خالی کنم.
نکات امنیتی
این اسکریپت با فایل واقعی سروکار دارد، بنابراین چند نکته مهم است.
اول اینکه بهتر است حداقل در شروع، آن را روی فایلهایی تست کنید که از حذف احتمالیشان مطمئن هستید.
دوم اینکه تغییر:
FileRecycle
به روش حذف دائمی را توصیه نمیکنم، مگر اینکه دقیقاً بدانید چه میکنید.
سوم اینکه اگر کد را تغییر میدهید، Validation مسیر فایل را حذف نکنید.
این قسمتها شاید باعث طولانیتر شدن کد شده باشند، اما دقیقاً همان بخشهایی هستند که یک Automation معمولی را به ابزاری مطمئنتر تبدیل میکنند.
محدودیتهای فعلی
نسخه فعلی برای مسئلهای که داشتم ساخته شده و قرار نیست یک ابزار مدیریت کامل Library باشد.
در حال حاضر موارد زیر جزو Scope پروژه نیستند:
- GUI
- انتخاب Hotkey از تنظیمات
- پشتیبانی رسمی از Playerهای دیگر
- مدیریت Playlist
- Undo داخلی
- Database
- History جداگانه فایلهای حذفشده
- Permanent Delete
- پردازش Batch آهنگها
هدف برنامه عمداً محدود است:
آهنگی را که همین حالا در AIMP پخش میشود، با یک Hotkey از هر جای ویندوز هرس کن.
نتیجه تست
اولین نسخه مستقیم پروژه، v1.0.0، در تست واقعی من کار نکرد.
بهجای کنارگذاشتن ایده، نسخه v1.1.0 با Logging و کنترلهای بیشتر ساخته شد.
نتیجه تست نسخه جدید:
Current Track detected
↓
Next Track
↓
Previous Track recycled
↓
PASS
و در استفاده واقعی، Ctrl + Delete همان Workflow موردنظر را فراهم کرد.
جمعبندی داریوش
این پروژه نمونه خوبی از Automationهای کوچکی است که شاید از نظر تعداد خطوط کد پروژه بزرگی نباشند، اما در استفاده روزمره تأثیر محسوسی دارند.
مسئله از یک نیاز خیلی ساده شروع شد: من حدود ۱۰۰ گیگابایت موسیقی جدید داشتم که باید بهمرور گوش میدادم و هرس میکردم، اما نمیخواستم برای حذف هر Track چند بار بین پنجرهها جابهجا شوم.

نتیجه یک اسکریپت AutoHotkey شد که:
Ctrl + Delete
را به این Workflow تبدیل میکند:
تشخیص Track فعلی AIMP
↓
اعتبارسنجی فایل
↓
رفتن به Track بعدی
↓
اطمینان از تغییر Track
↓
انتقال فایل قبلی به Recycle Bin
↓
ثبت نتیجه در Log
برای من مهمترین بخش پروژه این بود که Automation فقط سریع نباشد؛ باید تا جای ممکن قابل پیشبینی و امن هم باشد.
وقتی اسکریپتی قرار است با یک کلید سراسری روی فایلهای واقعی آرشیو موسیقی کار کند، این تفاوت اهمیت زیادی دارد.
لاگ واقعی پروژه
فایل Diagnostic ارسالی فقط یک نمونه ساختگی نیست؛ چهار اجرای واقعی Ctrl + Delete را ثبت کرده است. در هر چهار اجرا، Mapping با نام AIMP2_RemoteInfo با موفقیت خوانده شد، مسیر Track فعلی به دست آمد، فرمان Next ارسال شد و در پایان فایل قبلی با RECYCLE | PASS به Recycle Bin منتقل شد.
نکته مهمتر این است که در هر چهار نمونه، اولین بررسی بعد از NEXT | sent هنوز همان Track قبلی را نشان میدهد و Script مجبور شده وارد مسیر track unchanged -> retry شود. بنابراین Retry در این پروژه یک کنترل تزئینی یا فرضی نیست؛ در تمام Sampleهای ثبتشده همین Log عملاً فعال شده است. این چهار نمونه برای نتیجهگیری آماری درباره همه سیستمها یا همه نسخههای AIMP کافی نیست، اما رفتار محیط تست واقعی این پروژه را بهروشنی نشان میدهد.
[2026-08-24 16:36:17] HOTKEY | Ctrl+Delete
[2026-08-24 16:36:17] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=138
[2026-08-24 16:36:17] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:17] TRACK | D:\#Music\_20260819\music_20260818\PLn95MnWAC4Dc3qxAQI4XgGWk453vn5fxO\Knighthood Great Battle Nordic⧸Germanic⧸Scandinavian Folk Metal.opus
[2026-08-24 16:36:17] NEXT | sent
[2026-08-24 16:36:18] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=138
[2026-08-24 16:36:18] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:18] AFTER NEXT | D:\#Music\_20260819\music_20260818\PLn95MnWAC4Dc3qxAQI4XgGWk453vn5fxO\Knighthood Great Battle Nordic⧸Germanic⧸Scandinavian Folk Metal.opus
[2026-08-24 16:36:18] NEXT | track unchanged -> retry
[2026-08-24 16:36:19] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=136
[2026-08-24 16:36:19] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:19] RECYCLE | PASS | D:\#Music\_20260819\music_20260818\PLn95MnWAC4Dc3qxAQI4XgGWk453vn5fxO\Knighthood Great Battle Nordic⧸Germanic⧸Scandinavian Folk Metal.opus
[2026-08-24 16:36:42] HOTKEY | Ctrl+Delete
[2026-08-24 16:36:42] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=136
[2026-08-24 16:36:42] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:42] TRACK | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\THULCANDRA Frozen Kingdom (live) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:36:42] NEXT | sent
[2026-08-24 16:36:42] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=136
[2026-08-24 16:36:42] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:42] AFTER NEXT | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\THULCANDRA Frozen Kingdom (live) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:36:42] NEXT | track unchanged -> retry
[2026-08-24 16:36:43] REMOTE | size=88 active=-1 album=43 artist=15 date=4 filename=58
[2026-08-24 16:36:43] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:36:44] RECYCLE | PASS | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\THULCANDRA Frozen Kingdom (live) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:38:22] HOTKEY | Ctrl+Delete
[2026-08-24 16:38:22] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=165
[2026-08-24 16:38:22] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:38:22] TRACK | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\TEMPERANCE Breaking The Rules Of Heavy Metal (Official Video) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:38:22] NEXT | sent
[2026-08-24 16:38:22] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=165
[2026-08-24 16:38:22] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:38:22] AFTER NEXT | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\TEMPERANCE Breaking The Rules Of Heavy Metal (Official Video) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:38:22] NEXT | track unchanged -> retry
[2026-08-24 16:38:23] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=176
[2026-08-24 16:38:23] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:38:24] RECYCLE | PASS | D:\#Music\_20260819\music_20260818\UCG7AaCh CiG6pq rRDNw72A\TEMPERANCE Breaking The Rules Of Heavy Metal (Official Video) | Napalm Records Napalm Records Videos.opus
[2026-08-24 16:44:54] HOTKEY | Ctrl+Delete
[2026-08-24 16:44:54] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=134
[2026-08-24 16:44:54] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:44:54] TRACK | D:\#Music\_20260819\music_20260818\PLykNNMVjCDfWTLQqFMFHLNdNMWXEzfCRt\Bethorian At the Dusk of Ages (Full EP) Melodic Black Metal.opus
[2026-08-24 16:44:54] NEXT | sent
[2026-08-24 16:44:55] REMOTE | size=88 active=-1 album=0 artist=0 date=0 filename=134
[2026-08-24 16:44:55] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:44:55] AFTER NEXT | D:\#Music\_20260819\music_20260818\PLykNNMVjCDfWTLQqFMFHLNdNMWXEzfCRt\Bethorian At the Dusk of Ages (Full EP) Melodic Black Metal.opus
[2026-08-24 16:44:55] NEXT | track unchanged -> retry
[2026-08-24 16:44:56] REMOTE | size=88 active=-1 album=15 artist=6 date=10 filename=101
[2026-08-24 16:44:56] MAPPING | PASS | AIMP2_RemoteInfo
[2026-08-24 16:44:56] RECYCLE | PASS | D:\#Music\_20260819\music_20260818\PLykNNMVjCDfWTLQqFMFHLNdNMWXEzfCRt\Bethorian At the Dusk of Ages (Full EP) Melodic Black Metal.opus
این Log یک درس مهم برای Automationهای حساس دارد: موفقشدن ارسال Command با رسیدن Application به State موردنظر یکی نیست. وقتی مرحله بعدی قرار است File Operation انجام دهد، بهتر است State نهایی جداگانه Verify شود و در شرایط نامطمئن عملیات مخرب متوقف بماند.
درسهای فنی پروژه
این Script کوچک چند الگوی قابل تعمیم برای Automation در Windows دارد. اولین مورد جداسازی Command Delivery از State Verification است. SendMessage ممکن است بدون Exception تمام شود، اما Application مقصد هنوز State جدید را در Shared Memory منعکس نکرده باشد. نسخه 1.1.0 بهجای اعتماد کور به نتیجه Call، Track را دوباره میخواند.

دومین الگو، Fail Closed است. اگر Process وجود نداشته باشد، Mapping خوانده نشود، Path به Local File اشاره نکند، داده Memory نامعتبر باشد یا AIMP بعد از Retry Track را ترک نکند، Script بهجای حدسزدن متوقف میشود. برای Automationهایی که با فایل واقعی سروکار دارند، این رفتار معمولاً از تلاش برای «هر طور شده ادامه دادن» ارزشمندتر است.
سومین نکته، کمکردن وابستگی به UI است. این پروژه برای تشخیص فایل به Selection، Skin، Position عناصر یا Focus پنجره AIMP وابسته نیست. چنین طراحیای معمولاً نسبت به Click Automation مقاومتر است؛ هرچند در عوض به Contract مربوط به Shared Memory و Remote Interface AIMP وابسته میشود و باید در Major Versionهای آینده دوباره بررسی شود.
چهارمین نکته، انتخاب Recycle Bin بهعنوان Safety Net است. یک Global Hotkey همیشه احتمال فشردن اشتباه دارد. به همین دلیل حذف دائمی، حتی اگر از نظر کدنویسی سادهتر باشد، برای این Workflow انتخاب مناسبی نیست.
منابع مرجع
کد و نتیجههای این مقاله از پروژه و تست واقعی DJH آمدهاند؛ برای بررسی APIها و رفتارهای Version-sensitive نیز این منابع اصلی مفید هستند:
- AIMP SDK؛ مرجع اصلی برای بررسی SDK، Remote Interface و تغییرات نسخههای جدید AIMP.
- Microsoft Learn — WM_APPCOMMAND؛ مرجع رسمی Windows برای پیامهای Application Command و Media Commandها.
- AutoHotkey v2 Documentation؛ مرجع Syntax و Functionهایی مانند
DllCall،FileRecycle،SendMessageو Hotkeyها.
سوالات متداول
این پرسشها روی رفتار واقعی نسخه فعلی و محدودیتهای همین پروژه تمرکز دارند.
آیا اسکریپت بدون فعال بودن پنجره AIMP کار میکند؟
بله. Hotkey بهصورت Global تعریف شده و مسیر Track از Shared Memory خوانده میشود؛ بنابراین تشخیص آهنگ به Focus پنجره یا Select بودن آیتم Playlist وابسته نیست.
آیا فایل برای همیشه حذف میشود؟
خیر. نسخه فعلی از FileRecycle استفاده میکند و فایل را به Recycle Bin ویندوز میفرستد. Permanent Delete عمداً در Scope پروژه قرار نگرفته است.
اگر AIMP بسته باشد چه میشود؟
Script ابتدا وجود Process با نام AIMP.exe را بررسی میکند. اگر AIMP اجرا نباشد، عملیات متوقف، خطا Log و Tooltip/Beep نمایش داده میشود.
اگر Track یک Stream یا URL باشد چه میشود؟
اگر مقدار خواندهشده به Local File موجود اشاره نکند، FileExist اعتبارسنجی را رد میکند و چیزی حذف نمیشود.
چرا بعد از Next دوباره Current Track خوانده میشود؟
چون ارسال موفق Command لزوماً به معنی تغییر فوری State نیست. Log واقعی پروژه نشان داد در چهار اجرای ثبتشده، Check اول هنوز همان Track قبلی را میدید و Retry لازم شد.
آیا Ctrl + Delete قابل تغییر است؟
بله. تعریف ^Delete:: را میتوان با Hotkey دیگری در Syntax AutoHotkey v2 جایگزین کرد. برای چنین عملیات فایلی بهتر است میانبری انتخاب شود که احتمال فشردن تصادفی آن پایین باشد.
چرا از Plugin جانبی AIMP استفاده نشده است؟
برای هدف این پروژه Shared Memory اطلاعات کافی ارائه میکرد و Plugin یک Dependency اضافه ایجاد میکرد. اگر امکانات عمیقتر AIMP در آینده لازم شود، Plugin یا API کاملتر میتواند منطقیتر باشد.
آیا میتوان Script را برای Player دیگری استفاده کرد؟
الگوی کلی قابل تعمیم است، اما بخش دریافت Current Track کاملاً AIMP-specific است. برای Player دیگر باید API، IPC یا روش مطمئن دیگری برای بهدستآوردن Full Path فایل فعلی پیدا شود.
آیا Retryهای Recycle را میتوان حذف کرد؟
از نظر فنی بله، اما توصیه نمیشود. این Retryهای کوتاه برای حالتی هستند که Decoder یا File Handle با کمی تأخیر آزاد میشود و حذف آنها میتواند Failureهای موقتی را بیشتر کند.


