Skip to content

Instantly share code, notes, and snippets.

@CannoHarito
Last active March 10, 2024 13:03
Show Gist options
  • Save CannoHarito/b0fc7d03442f0d5c8031b298dde019b8 to your computer and use it in GitHub Desktop.
Save CannoHarito/b0fc7d03442f0d5c8031b298dde019b8 to your computer and use it in GitHub Desktop.
win10用 httpサーバ経由でiTunesプレイリストを再生するpowershell

iTunesServer.ps1

Windows10用 httpサーバを立て、リクエストに応じて、iTunesでプレイリストを再生するPowershellスクリプト

Description

.NET (Framework4~, Core3~) のSystem.Net.HttpListenerで簡易webサーバを立てて、受け取ったhttpリクエストにより、COMインタフェース(ActiveX)を使ってWindows版iTunesを操作する。

ゲームStormworksでカーオーディオを実装するために作った。Steamワークショップ

Demo

とりあえず試す場合は、powershellを起動し以下1行を実行する。

iwr https://gist.githubusercontent.com/CannoHarito/b0fc7d03442f0d5c8031b298dde019b8/raw/iTunesServer.ps1|iex

iTunesが起動する。ブラウザで http://localhost:18180/ にアクセスするとパラメータ取得用ページが表示される。

Requirement

  • PowerShell 5.1
  • iTunes(for windows)

試していないが、pwsh6以降でも動くはず。
Macにpwsh(.Net Core)を入れてもiTunesが対応しないので、たぶん動かない。

Usage

現在は5つの機能がある。

  • http://localhost:18180/
    ユーザープレイリストの一覧をhtmlで表示する。
    Stormworksのマイコンに書き込む文字列を作るインタフェースとなっている。
    ゲーム内の私が作ったマイコンでは、プレイリスト名をascii 6文字しか表示できない。
  • http://localhost:18180/playlists
    ユーザプレイリストの一覧をjson形式で取得する。イカシタhtmlは用意できてない。
    Idに書かれている16文字を、下の項目で使用する。
    Kindの値は、プレイリストの種類 0:List,1:Smart,2:Folder,3:AllMusic を示している。
    応答の例
[{"Name":"ミュージック","Kind":3,"Id":"C273F391ED36ED58"},{"Name":"bot","Kind":2,"Id":"B0CC1FE45CDEAE04"},{"Name":"丹下桜","Kind":1,"Id":"03A266A399A4BE70"},{"Name":"むつらぼし","Kind":0,"Id":"FE0E5FEF55D12BE0"}]

サーバを止めるときは、Ctrl+Cを入力する。この時、リソースの解放が行われる。 リソース解放前にiTunesを終了しようとすると、iTunesからエラーメッセージが出る。先にサーバを止めることをお勧めする。

Install

Open PowerShell
Run the following 3 commands:

iwr https://gist.githubusercontent.com/CannoHarito/b0fc7d03442f0d5c8031b298dde019b8/raw/iTunesServer.ps1 -OutFile iTunesServer.ps1;
Unblock-File .\iTunesServer.ps1;
echo "powershell.exe -NoProfile -ExecutionPolicy RemoteSigned .\iTunesServer.ps1" |Out-File iTunesServer.bat -Encoding ascii

you can start server by Double click iTunesServer.bat.

Licence

パブリックドメインとする。使用者の責任で使用される。

Author

@canno_harito

param([int]$port = 18180)
$url = "http://localhost:$port/"
function Invoke-ITunesServer {
param([Parameter(Mandatory)][string]$Url)
$iTunesApplication = Get-ITunesApplication
$iTunesPlaylists = $iTunesApplication.LibrarySource.Playlists
$listener = New-Object Net.HttpListener
$listener.Prefixes.Add($Url)
Write-Host "Listening... at $Url"
Write-Host "Press Ctrl-C to quit."
$listener.Start()
try {
while ($true) {
$contextTask = $listener.GetContextAsync()
while (-not $contextTask.AsyncWaitHandle.WaitOne(200)) { }
$context = $contextTask.GetAwaiter().GetResult()
$request = $context.Request
$response = $context.Response
Write-Host $request.RawUrl
$localPath = $request.Url.LocalPath
if ($localPath -eq "/start") {
if (($id = $request.QueryString.Get("id")) -and "$id".Length -eq 16) {
Start-Playlist $id ("shuffle" -in $request.QueryString.Keys) ("repeat" -in $request.QueryString.Keys)
}
else {
$response.StatusCode = 403
}
}
elseif ($localPath -eq "/stop") {
Stop-Playing
}
elseif ($localPath -eq "/volume") {
if ($add = $request.QueryString.Get("add") -as [int]) {
Add-SoundVolume $add
}
}
elseif ($localPath -eq "/playlists") {
$content = [System.Text.Encoding]::UTF8.GetBytes((Get-All-UserPlaylists | ConvertTo-Json -Compress))
$response.ContentType = "application/json"
$response.OutputStream.Write($content, 0, $content.Length)
}
elseif ($localPath -eq "/") {
$content = [System.Text.Encoding]::UTF8.GetBytes((Get-Html (Get-All-UserPlaylists | ConvertTo-Json -Compress)))
$response.ContentType = "text/html"
$response.OutputStream.Write($content, 0, $content.Length)
}
else {
$response.StatusCode = 404
}
$response.Close()
}
}
finally {
$listener.Close()
Write-Verbose "release-comobject at end"
while ([Runtime.Interopservices.Marshal]::ReleaseComObject($iTunesPlaylists)) { Write-Verbose "retry release playlists" }
$iTunesPlaylists = $null
while ([Runtime.Interopservices.Marshal]::ReleaseComObject($iTunesApplication)) { Write-Verbose "retry release itunesapplication" }
$iTunesApplication = $null
[GC]::Collect()
}
}
function Get-ITunesApplication {
New-Object -ComObject iTunes.Application
}
function Get-All-UserPlaylists {
$iTunesPlaylists | ForEach-Object {
if ($_.SpecialKind -in 0, 4, 6) {
#0:No special kind,4:Folder playlist,6:Music playlist
[pscustomobject] @{
Name = $_.Name
Kind = @{0 = 0; 1 = 1; 5 = 2; 7 = 3; }[$_.SpecialKind + [bool]$_.Smart] #0:List,1:Smart,2:Folder,3:AllMusic
Id = $iTunesApplication.ITObjectPersistentIDHigh($_).ToString("X8") `
+ $iTunesApplication.ITObjectPersistentIDLow($_).ToString("X8")
}
}
[void][Runtime.Interopservices.Marshal]::ReleaseComObject($_)
}
[void][Runtime.Interopservices.Marshal]::ReleaseComObject($iTunesPlaylists)
}
function Start-Playlist {
param(
[Parameter(Mandatory)][ValidateLength(16, 16)][string]$PersistentId,
[bool]$ForceShuffle = $false,
[bool]$ForceRepeat = $false
)
$playlist = $iTunesPlaylists.ItemByPersistentID(
"0x$($persistentId.Substring(0,8))", "0x$($persistentId.Substring(8,8))"
)
if ($playlist) {
if ($ForceShuffle) {
Write-Verbose "set shuffle"
$playlist.Shuffle = $true #$true,$false
}
if ($ForceRepeat) {
Write-Verbose "set repeat"
$playlist.SongRepeat = 2 #0:play once,1:track repeat,2:playlist repeat
}
$playlist.PlayFirstTrack()
[void][Runtime.Interopservices.Marshal]::ReleaseComObject($playlist)
$playlist = $null
}
}
function Add-SoundVolume {
param([int]$Add = 10 )
$volume = $iTunesApplication.SoundVolume + $Add
$iTunesApplication.SoundVolume =
if ($volume -le 0) { 0 }
elseif ($volume -ge 100) { 100 }
else { $volume }
$iTunesApplication.SoundVolume
}
function Stop-Playing {
$iTunesApplication.Pause()
}
function Get-Html {
param ([string]$json = "[]")
@'
<!doctype html>
<meta charset="utf-8">
<title>iTunes Server Setting</title>
<style type="text/css">
*{box-sizing:border-box}body{max-width:60em;padding:1em;margin:auto;font:1em/1.6 sans-serif}table,textarea{width:100%}input[type=text]{padding:.5em;border:solid #eee;width:100%}td,th{padding:.5em;text-align:left;border-bottom:solid #eee}tr.checked>td{border-bottom-color:#fa0}td.up{width:4em}tr.checked:nth-child(n+2)>td.up:before{content:"\01F53A"}
</style>
<h1>&#x1F3B5;Select playlists to display in Stormworks</h1>
<textarea></textarea>
<form><table><thead><tr><th> &#x2705; Playlist<th>Display Name (only ASCII characters)<th><tbody></table></form>
<script type="application/json" id="json-playlists">
%json%
</script>
<script>
const e=(e,t=document)=>t.querySelector(e),t=(e,t=document)=>t.querySelectorAll(e),n=["&#x1F4DD;","&#x1F505;","&#x1F4C1;","&#x1F3B6;"],c=JSON.parse(e("#json-playlists").textContent),a=e("tbody"),r=e("form"),o=()=>{e("textarea").value=[...new FormData(r).entries()].map(([e,t])=>`${t},${e}`).join(",")};c.forEach(({Name:e,Kind:t,Id:c},r)=>a.insertAdjacentHTML("beforeend",`<tr name="${r}"><td><label><input type="checkbox">${n[t]+e}</label><td><input type="text" name="${c}" pattern="[!-~]+" value="${(e.replace(/[^\x21-\x7e]+/g,"")||"list"+r).toUpperCase()}" disabled><td class="up">`)),t('input[type="checkbox"]',a).forEach(n=>n.addEventListener("change",n=>((n,c=!0)=>{const r=e('input[type="text"]',n);if(e('input[type="checkbox"]',n).checked)n.classList.add("checked"),r.disabled=!1,a.insertBefore(n,e("tr:not(.checked)",a)),c&&r.focus();else{n.classList.remove("checked"),e('input[type="text"]',n).disabled=!0,$notcheck=t("tr:not(.checked)",a);const c=n.getAttribute("name")-0;let r=null;for(let e=0;e<$notcheck.length;++e)if($notcheck[e].getAttribute("name")-0>c){r=$notcheck[e];break}a.insertBefore(n,r)}})(n.target.parentElement.parentElement.parentElement))),t("td.up").forEach(e=>e.addEventListener("click",e=>{const t=e.target.parentElement;t.classList.contains("checked")&&t.previousSibling&&(a.insertBefore(t,t.previousSibling),o())})),r.addEventListener("change",e=>o()),r.addEventListener("submit",e=>e.preventDefault())
</script>
'@ -replace "%json%", $json
}
if ($MyInvocation.InvocationName -ne ".") {
# $VerbosePreference = 'Continue'
# Write-Host $url
# $iTunesApplication = Get-ITunesApplication
# $iTunesPlaylists = $iTunesApplication.LibrarySource.Playlists
# Get-All-UserPlaylists | ConvertTo-Json -Compress
# Start-Playlist "8F84264DF04A814B" $true $true
# Add-SoundVolume 0
# Stop-Playing
# [Runtime.Interopservices.Marshal]::ReleaseComObject($iTunesPlaylists)
# $iTunesPlaylists = $null
# [GC]::Collect()
# [Runtime.Interopservices.Marshal]::ReleaseComObject($iTunesApplication)
# $iTunesApplication = $null
# [GC]::Collect()
Invoke-ITunesServer $url
}
-- input:from touchdata
-- * bool.ch1: isTouch
-- * number.ch3: tx touchX
-- * number.ch4: ty touchY
-- * number.ch7: time clock
-- property:
-- * number:"port" = 18180
-- * text:"play option" ="&shuffle=&repeat="
-- * text:"playlists" = "BGM,B0CC1FE45CDEAE04,MTRBS,FE0E5FEF55D12BE0,SAKURA,03A266A399A4BE70"
-- output:
-- * video: 32x32
local Playlists = {}
for str in (property.getText("playlists")):gmatch("[^,]+") do
Playlists[#Playlists + 1] = str
end
-- {"BGM", "B0CC1FE45CDEAE04", "MTRBS", "FE0E5FEF55D12BE0", "SAKURA", "03A266A399A4BE70"}
local Port, PlayOption = property.getNumber("port"), property.getText("play option")
local igN, igB, PlNum, Busy, Touching, Clock, Audio = input.getNumber, input.getBool, 1, false, false, "00:00", false
local isIn = function(tx, ty, x, y, w, h)
return tx >= x and tx <= x + w and ty >= y and ty <= y + h
end
local itStart = function()
async.httpGet(Port, "/start?id=" .. Playlists[PlNum + 1] .. PlayOption)
Busy = true
end
local itStop = function()
async.httpGet(Port, "/stop")
end
local itVolume = function(str)
async.httpGet(Port, "/volume?add=" .. str)
end
function onTick()
local time = igN(7)
Clock = string.format("%02.0f:%02.0f", time * 24 // 1, time * 24 * 60 % 60 // 1)
if igB(1) then
if not Busy and not Touching then
Touching = true
local tx, ty = igN(3), igN(4)
if isIn(tx, ty, 0, 8, 32, 7) then
Audio = not Audio
if Audio then
itStart()
else
itStop()
end
elseif isIn(tx, ty, 0, 16, 32, 7) then
PlNum = PlNum + 2 < #Playlists and PlNum + 2 or 1
if Audio then
itStart()
end
elseif Audio and isIn(tx, ty, 0, 25, 15, 7) then
itVolume("-10")
elseif Audio and isIn(tx, ty, 17, 25, 15, 7) then
itVolume("10")
end
end
else
Touching = false
end
end
function onDraw()
local s = screen
local color, text, textBox, line, rectF = s.setColor, s.drawText, s.drawTextBox, s.drawLine, s.drawRectF
color(200, 200, 200)
textBox(0, 0, 32, 7, Clock, 0, 0)
if Audio then
color(10, 180, 10)
else
color(10, 10, 10)
end
rectF(0, 8, 32, 7)
textBox(0, 16, 32, 7, Playlists[PlNum]:sub(1, 6), 0, 0)
textBox(0, 25, 32, 7, "- +", 0.1, 0)
line(0, 25, 32, 25)
line(16, 25, 16, 32)
color(0, 0, 0)
text(5, 8, "Aud")
text(19, 8, "i")
text(22, 8, "o")
if Busy then
color(0, 0, 0, 180)
rectF(0, 8, 32, 25)
end
end
function httpReply(res_port, request_body, response_body)
if response_body:match("^connect") then
Audio = false
end
Busy = false
end
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned .\iTunesServer.ps1
@CannoHarito
Copy link
Author

Invoke-Expressionで実行するスクリプトファイルの引数の与え方

iwr https://gist.githubusercontent.com/CannoHarito/b0fc7d03442f0d5c8031b298dde019b8/raw/iTunesServer.ps1|%{iex "&{$_} 8080"}

別の方法があれば教えてください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment