pax_global_header00006660000000000000000000000064142611676230014522gustar00rootroot0000000000000052 comment=c6bb67ada89c8808bdf2b790e81bed5cb3b23c84 dms-1.5.0/000077500000000000000000000000001426116762300123105ustar00rootroot00000000000000dms-1.5.0/.circleci/000077500000000000000000000000001426116762300141435ustar00rootroot00000000000000dms-1.5.0/.circleci/config.yml000066400000000000000000000033121426116762300161320ustar00rootroot00000000000000version: 2 jobs: build: machine: true environment: GO_BRANCH: release-branch.go1.16 steps: - run: echo $CIRCLE_WORKING_DIRECTORY - run: echo $PWD - run: echo $GOPATH - run: echo 'export GOPATH=$HOME/go' >> $BASH_ENV - run: echo 'export PATH="$GOPATH/bin:$PATH"' >> $BASH_ENV - run: echo $GOPATH - run: which go - run: go version - run: | cd /usr/local sudo mkdir go.local sudo chown `whoami` go.local - restore_cache: key: go-local- - run: | cd /usr/local git clone git://github.com/golang/go go.local || true cd go.local git fetch git checkout "$GO_BRANCH" [[ -x bin/go && `git rev-parse HEAD` == `cat anacrolix.built` ]] && exit cd src ./make.bash || exit git rev-parse HEAD > ../anacrolix.built - save_cache: paths: /usr/local/go.local key: go-local-{{ checksum "/usr/local/go.local/anacrolix.built" }} - run: echo 'export PATH="/usr/local/go.local/bin:$PATH"' >> $BASH_ENV - run: go version - checkout - restore_cache: keys: - go-pkg- - restore_cache: keys: - go-cache- - run: go mod download - run: go test -v -race ./... -count 2 -bench . - run: go test -bench . ./... - run: set +e; CGO_ENABLED=0 go test -v ./...; true - run: GOARCH=386 go test ./... -count 2 -bench . - save_cache: key: go-pkg-{{ checksum "go.mod" }} paths: - ~/go/pkg - save_cache: key: go-cache-{{ .Revision }} paths: - ~/.cache/go-build dms-1.5.0/.github/000077500000000000000000000000001426116762300136505ustar00rootroot00000000000000dms-1.5.0/.github/dependabot.yml000066400000000000000000000002421426116762300164760ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" dms-1.5.0/.github/goreleaser.yml000066400000000000000000000021521426116762300165230ustar00rootroot00000000000000# .goreleaser.yaml builds: # You can have multiple builds defined as a yaml list - # ID of the build. # Defaults to the project name. id: "my-build" # Optionally override the matrix generation and specify only the final list of targets. # Format is `{goos}_{goarch}` with optionally a suffix with `_{goarm}` or `_{gomips}`. # This overrides `goos`, `goarch`, `goarm`, `gomips` and `ignores`. targets: - linux_amd64 - linux_386 - linux_arm_6 - linux_arm_7 - darwin_arm64 - darwin_amd64 - windows_arm - windows_amd64 - windows_386 # By default, GoRelaser will create your binaries inside `dist/${BuildID}_${BuildTarget}`, which is an unique directory per build target in the matrix. # You are able to set subdirs within that folder using the `binary` property. # # However, if for some reason you don't want that unique directory to be created, you can set this property. # If you do, you are responsible of keeping different builds from overriding each other. # # Defaults to `false`. #no_unique_dist_dir: true dms-1.5.0/.github/workflows/000077500000000000000000000000001426116762300157055ustar00rootroot00000000000000dms-1.5.0/.github/workflows/docker-publish.yml000066400000000000000000000025761426116762300213550ustar00rootroot00000000000000name: Publish Docker image on: push jobs: buildx: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Docker meta id: meta uses: docker/metadata-action@v4 with: images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} # generate Docker tags based on the following events/attributes tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} # Required if building multi-arch images - name: Set up QEMU uses: docker/setup-qemu-action@v2 - id: buildx name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 with: buildkitd-flags: --debug install: true - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v3 with: platforms: linux/amd64,linux/arm64,linux/386 push: true labels: ${{ steps.meta.outputs.labels }} tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max dms-1.5.0/.github/workflows/go.yml000066400000000000000000000050261426116762300170400ustar00rootroot00000000000000name: goreleaser on: pull_request: push: permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser version: latest args: release --rm-dist --config ${{ github.workspace }}/.github/goreleaser.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - name: Upload artifact dms_linux_amd64 uses: actions/upload-artifact@v2 with: name: dms_linux_amd64 path: dist/my-build_linux_amd64/dms - name: Upload artifact dms_linux_386 uses: actions/upload-artifact@v2 with: name: dms_linux_386 path: dist/my-build_linux_386/dms - name: Upload artifact dms_linux_arm_6 uses: actions/upload-artifact@v2 with: name: dms_linux_arm_6 path: dist/my-build_linux_arm_6/dms - name: Upload artifact dms_linux_arm_7 uses: actions/upload-artifact@v2 with: name: dms_linux_arm_7 path: dist/my-build_linux_arm_7/dms - name: Upload artifact dms_darwin_arm64 uses: actions/upload-artifact@v2 with: name: dms_darwin_arm64 path: dist/my-build_darwin_arm64/dms - name: Upload artifact dms_darwin_amd64 uses: actions/upload-artifact@v2 with: name: dms_darwin_amd64 path: dist/my-build_darwin_amd64/dms - name: Upload artifact dms_windows_arm.exe uses: actions/upload-artifact@v2 with: name: dms_windows_arm path: dist/my-build_windows_arm/dms.exe - name: Upload artifact dms_windows_amd64.exe uses: actions/upload-artifact@v2 with: name: dms_windows_amd64 path: dist/my-build_windows_amd64/dms.exe - name: Upload artifact dms_windows_386.exe uses: actions/upload-artifact@v2 with: name: dms_windows_386 path: dist/my-build_windows_386/dms.exe dms-1.5.0/Dockerfile000066400000000000000000000006101426116762300142770ustar00rootroot00000000000000FROM golang RUN \ apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ ffmpeg \ ffmpegthumbnailer \ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ touch /root/.dms-ffprobe-cache COPY . /go/src/github.com/anacrolix/dms/ WORKDIR /go/src/github.com/anacrolix/dms/ RUN \ go build -v . ENTRYPOINT [ "./dms" ] dms-1.5.0/LICENSE000066400000000000000000000030071426116762300133150ustar00rootroot00000000000000Copyright (c) 2012, Matt Joiner . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.dms-1.5.0/README.rst000066400000000000000000000061251426116762300140030ustar00rootroot00000000000000dms === .. image:: https://circleci.com/gh/anacrolix/dms.svg?style=svg :target: https://circleci.com/gh/anacrolix/dms dms is a UPnP DLNA Digital Media Server. It runs from the terminal, and serves content directly from the filesystem from the working directory, or the path given. The SSDP component will broadcast and respond to requests on all available network interfaces. dms advertises and serves the raw files, in addition to alternate transcoded streams when it's able, such as mpeg2 PAL-DVD and WebM for the Chromecast. It will also provide thumbnails where possible. dms uses ``ffprobe``/``avprobe`` to get media data such as bitrate and duration, ``ffmpeg``/``avconv`` for video transoding, and ``ffmpegthumbnailer`` for generating thumbnails when browsing. These commands must be in the ``PATH`` given to ``dms`` or the features requiring them will be disabled. .. image:: https://i.imgur.com/qbHilI7.png Installing ========== Assuming ``$GOPATH`` and Go have been configured already:: $ go get github.com/anacrolix/dms Ensure ``ffmpeg``/``avconv`` and/or ``ffmpegthumbnailer`` are in the ``PATH`` if the features depending on them are desired. To run:: $ "$GOPATH"/bin/dms Running DMS as a systemd service ================================= A sample systemd `.service` file has been `provided `_ to assist in running DMS as a system service. Running DMS as a FreeBSD service ================================ Install the `provided `_ service file to /etc/rc.d or /usr/local/etc/rc.d add ``dms_enable="YES"``, and optionally ``dms_root="/path/to/my/media"`` and ``dms_user="myuser"`` to your /etc/rc.conf Known Compatible Players and Renderers ====================================== * Probably all Panasonic Viera TVs. * Android's BubbleUPnP and AirWire * Chromecast * VLC * LG Smart TVs, with varying success. Usage of dms: ===================== .. list-table:: Usage :widths: auto :header-rows: 1 * - parameter - description * - ``-allowedIps string`` - allowed ip of clients, separated by comma * - ``-config string`` - json configuration file * - ``-deviceIcon string`` - device icon * - ``-fFprobeCachePath string`` - path to FFprobe cache file (default "/home/efreak/.dms-ffprobe-cache") * - ``-forceTranscodeTo string`` - force transcoding to certain format, supported: 'chromecast', 'vp8' * - ``-friendlyName string`` - server friendly name * - ``-http string`` - http server port (default ":1338") * - ``-ifname string`` - specific SSDP network interface * - ``-ignoreHidden`` - ignore hidden files and directories * - ``-ignoreUnreadable`` - ignore unreadable files and directories * - ``-logHeaders`` - log HTTP headers * - ``-noProbe`` - disable media probing with ffprobe * - ``-noTranscode`` - disable transcoding * - ``-notifyInterval duration`` - interval between SSPD announces (default 30s) * - ``-path string`` - browse root path * - ``-stallEventSubscribe`` - workaround for some bad event subscribers dms-1.5.0/TODO000066400000000000000000000006431426116762300130030ustar00rootroot00000000000000* Reintegrate ffprobe error suppression into the ffmpeg.Probe function * Replace panics with proper error handling throughout the codebase. * Move ./dlna/dms somewhere more appropriate. It's moreof a DMS than a DLNADMS now. * Fix seeking for transcodes. Should be broken. * DMS handler path /icon should be /thumbnail, and /deviceIcon->/icon, or something like that. * Work around lack of ffmpegthumbnailer on Windows. dms-1.5.0/data/000077500000000000000000000000001426116762300132215ustar00rootroot00000000000000dms-1.5.0/data/VGC Sonic.png000066400000000000000000000547571426116762300154240ustar00rootroot00000000000000PNG  IHDR>a IDATxweUs}r*+ Ng[4h 4 OMcLj==& ccX,IP%U)TK7svX'xlGwZ[uq]\uq]\uq]\uq]\uq]\uq]\uq]\uq]\uq]\pMg%QЀbMv( 1Jc4!S D$}Z"E0D@433͛oze} k;Z1(E$`m:kM~ۈo#(&:. 14 IUQ:Ъ Mah==oz/1/KC5B1*"@aVVA/5$h` GH)BRI)Jz|,[x m}m+oݘ,K/ߛ9}1&Oh\ d:/x0kVII(_`u~DdO61зe?ؗ4KV, D!M1[IVQ>v6 HtyR(0|"f^!YB~m w_X_T-D l>$t:JaW%DhY܇$K iGl5, A#(c/YeJB@YZ* =KiE|xl' !0l !]y!HҌBk ژJ(Bk=փy} 0b{UA ~UbКQD.(Bɶ -P utB - DDAl P(R`U)$4Vm+m?%)>[׬ DXT(%l].6GI鱬>V"d1|D8apJ4M ĈI&"6bV*Ta?%))wOLAȤ>aI Y m Yp 1CHB(zŨˣ>FJYژ0ViMBD!Ϡ>gXx~O>;cG'~d &/3yli4=-ϠO尯[qBkcBNҢTMJ4Sm/\@ %) Y692hqRuɹmeA`‡@ VLC %m ![5&HBm=1 >k-oGl|zܤwhD\UAB$ wlbFBaH 6Α# cR)!QF0 C\ȡRq͇NLU)(0C'$AUBA>$>*E$)c5 MO L]5*+u*j)AHHYaJ){j6B:㫊G{e+Fq׉k?>{_XQctL**m82d_1SZ(BiYMfmR1)00;*RA1L&| qA(dPAH! Iq:ֆ$Nj#TJI{* cRHF" %a_2nBl MUZ|(% ՊL܎c̷?e)ʨx 1J%d@4OF 1D%`0a$FD}d-:ӜY]1B!R"*(**JS f) Mh)ƍ-(*X&dBHqJhSFMVbh$EH^R)|Q'AyOHZYZn;(ks))I@U$砕B5Q{%g(mV˚6w=r"xcKv c(mP#BF6:D*0q4q/am84Mq֒5D8Rh*"HY@بPoA透`|/y !CD҂m=yTïZI0W 2+AmGիpshA|?OʟEu3%"0caPà2KMe"m9tNnȘA Ae0Z%B(@\ grJE|)tS`] dOІ(Ƙ \&}ÿB)U/ j@g[(C F/o0SY9ݽ~Ww#ٕp;g]mCNohz2f23sx_~9(%s'/|$R(o 78/>í4+:[h Ąb$G>AĤ,W0=hzFg15LƎ 9|')MI3UL,[Z11&t%Mլ DkEAI:6WbLaoHT ;P\~YWٲDII"4Pta3}F舷b۶ض}2.-!ƈhL]Ovs핬YuںOP_WFyo܀7߸uk`04!44$2Ԛ\( kQv{fXPbJ!g7f8NJS\d4Bv%^RvfU[P BL$‚*%*)Ƥ_Z!"S7-4M), 煪TV묈5" Xhꄹ^a+ WGL#- ,)rxLO5p94W^KRNSYE&HIT\w w=[W Mpק< t H50VU-űEá\o;f fÄBBDIFu+)-lxbF]T$c,)1U[:DUQ+}VNJ]#)MAXE [Ec;ZH QN3enC!r~mHld4dfO Phhvi7B_?K1]׉ʊH׵bvl 0,E(4l,9mQ O֨14Y3\\oe󞟩|RAԖ7I2*anD:USK$+ZvYjݩd3WT`jFӺXfRv&.KmQSH:PK6F+[ :qE0pMMLF 7)LVz*4M)e!Õyr~jv~ݟY=ۮDu;zq uc Ç:ͯ}u:9[y DP(%=\f'l4/~$ )W?q) ;21>&]D)Ez.2HMմNUen ]_F ZPD\.wR!15-m[65ZIqpk︎kBmwT`6`u'!8=X_/|s8::~rHa`C} Vߍj/_Ovuw,.(va=r'{C/#˴Ԙ##c\>F4*MFZ>zw!ak2NċϑCpjzUg Huέx"gse@V+3Po\&d!b6)Hf#言 &8X kM/)P+_,G )OTbJQiJ?U_amu B9xk%ؽm;w3 )lNO^:]P-FOC-&[+06- ';P[t$LiOJXcyfaPYX#)Tθ:(a,~[՘̘!ńl -xv휥73Gߧv?Bfw7M ͓M q}eJ:})…Y(9S\NT{9SD`_Nz^qAJ [qG~F!jb8IlWZLLx֔hvO)Rmef Gmo!缞wi~gPP(V@6&0:k}Zafv;̄}G)`0a>eUa(ʊA7"(tB2VSWXDER]͛+Ya\.j=BE/4`ʘX0iBe 2}:dqaҷ ٙE2ՑiF-ow_([H@Fm-Zk 5K_e70wO"[m|f_$ӲUCo} x'! fsuH?I`oN ƍ){=v^Cs.z袠*J-S@mDar>zNlRE'NO\IFe(Mr;)4 Q(fi}]_K\M*MS8c. Q4^s~:B!g(AO_/QVrtkRGt'NDqEYH iw5TT!E@͔ y[ce!|6O/I5nɉ\jP "U !obߞmr2[59xk`*jKA."t kbuxVg .M(Yf Esm;V[@[«m 4"Tp>AxܘD؄Mu]N_}C$Эvt$#[zaPhBù@[*\X+ q%téN!>\AH{!2ıwgc Z]3x^/ox[xf1bϮ>Tj((-%ZnLi&CÚ cNp+F;`$pn^PTFOOe𝂄\O!eRIؒ~9{\$Tgi!U u+M`:,"C]9Ñ\L*B.}4[*UnMDp>ysg$ss-+c|(IiR$sGIho|>)4~7gnc `J*4/f :b 1KK#Nc:preieDDd>N4 $NDqjK&9*'҆;/aKosc>f>&%b8NcpSe71q :৲Rj%DR0ʖÝjY;!ԫ^<Bw.&+CS0FBL%Jr̀{ύW"{{#ү)g-O^}1pO$dFB҉:c UDU2deCJVXW/ { a!UA1؁a $V%D#5iIJazR)h^8$Xś> #ES_Eq똫*q9Y*jN>$jz3-DV `Y#DZa-4uD| T}W 3L\­GRy7)p/'P8h5$uyTi  =jjf}Yhԩh(QCTC$5:ގLWEJ!άwjs/mjJn Y(¸NBL28UaqTrM݀ pitI)`Ўd[OO5Dĭ:͇ ) % ZSFǨ CFNN>^J8v^Aӷ'8o;wjήJ#SO$|l_p"T hopuhªrgj<5ɣ55'P-RԿc}zc>MQRK[Wqu9q6ڜ铌\}H{$w8iCLGlwK6.&)IPZBJ#S)Xqډ[iT *\ 1Ӡ4Ap̻`%o=ĠwߢizOd\opQkuhZh h'jdƈGW8~x3 Zte`73$eh;Xuuwz0Ƙck֧|lPZ 0TjwL6!̔g ^0ZD}ИHWRŤnLfIe\(s@kJ$+B4c(p  mo/z=59榑kGtT&昭gjY%ЌSTC~v W#~s/o"q.5yL9Z}` IDAT 9;$?/K\bKf{EL1>)6u3 VTz3'и宙iR0MwSוVI!?qL`{QOL.VQ_IQt (ϖL0 wM_w T[$pЦVХt. w]Э/e/|G͹-N ETS+0-C6q3&Hޥ}u6ONb-4:}:,ߓ,թ,(^g5 ڨSI(VSgN1buf+c +1h2۷ʲ5WO_uu`χ. n= g'o7')up9$Brp_}11'OXL W1sę}l"3UAóLLO~aSHTxHl77Yu!Ӹ[}43:DFIKK-4_ۋM =F%iBE beRjJ؞Ҭe-ǿ# W_~?_"]ӻ`m4ChPwckFSp;J!ϟ'8|/w?'Vx!Wf}'&E)s #GQxZ ;77:3L =XN=[3*?1ˉ*gqkԖ:9QBPG?s-Z! M èu9I+Ƣ9\[Z ^<^[0gO" X^vK_k)8/^s}~FX<qM25Nv(hPOwro`oxϚo80+O"Si/Ke6S@Š[v*ܥ7dJS{1N#@r'm>jB&6n?Y- B ~o{|60٧Ibn}WiFݘ?yw.⛟O1я|/Կ仿gyz__ 5K7^ZN~3; ,O*o= ur jP冢h~9m.Wd4Z…qCߓxǸ{Ռkq{h:  /1\㱉 2,+V eRpi NP,I>Z)Jmh%Q;.'FSs8z=,v׺B2H /V!Kgh>ڸEià8S;\4:l "CAE(jST t[;233ҹ;u[ܖs>z nA̅?A=6=D1 af \>R,{?E cfs܀'1*}v g'h[?w f'ϝ]|t>Ze6R.Ⅿ*^%¸NT(ic`mT{$7w18.YI*#&7;Ӝ}D.+QFud\s5$̬"hSYTZv\Žm̛ N^hm(3l#fyW(ǧ:mW2jc,nxQj!" }O .a:ȿy>(w} 9? (K\=70UhՓD.-iceIP4mc'B5Wm+alp_xa܃`׵afbAbE.d0۳ 8_DN~v\n `ϼW\7,kh EWо`ws=F'"%EէЅET~1M&NT}z9JZe P0ef f+td@GuU2!i&]O@tF53EX/z_ Sᯮ?g>wccg~w8OZɣj6\1gnc37AX8E6y N6& LGĘx4khCؕkBRI (%%rVU<KUlce9$x!TW{&'s(R*-S9Ɣ2R)p} 1eܻY{ ˗eW\ƞ=;ؾ}' }ED98wΟΛU{ȱGx /ɿX-> Q!evmX˦[fAS,I AcX짓 Rhll3՚zs}a/BHƄ3ifr̖EN0&àW&v#YG_誏uRi>PL(d4rL3BdO1&7+l)𴜓KW>2rO3aVWF|jxfg{S4Lʛ%W48,Y^0OKЇUΞ?͋n*zQ=fXف֊9/g3fmՋ1&J[DR@0lZ1/PU%A!@r^:rbV"mL…J ^΅dz8BЭ#)XNz$ ӎ&2R*$l4^`BB DrW2'gwq‰ճ.R}U0#y#p!8 \wj4xmbaK˟> kЇEF?E%.9Ġ7Goa\SelNC4_Or^N_4i&!;/P"^U`Izz~h˂ϧ⭋(9ʥOyBmw5z&Wtd75"B==S 6Yƥii%ݠAYSgeB,3`GO7CV6u O*VgJ%UUלg>M}=fz E ̜?A+o7}滛gy]mī_gQ>+g51:$Woߛ3+pQh캍9u i&jI)(oE(Ln%E e䑕s4( eicu7JVMseq5gû"36ലhNMd 1Mw>Oq-dHN<{ՈŊK_|cT,.ڵI =h>vn;{Kؿțov?|g23c8 .\s'8d]KY0Eem=UH됢-irEB-.Ǜh?0;NcO}O;G>\2ώ`0yއx:9_{glWv wxnu#n4S@+(BBW1&TUT(+)$L m-n-Ԓxsa}h5$q[s;{5ZE@k9e>HA9{'}) df3#dr+ NH 4jl;64|+6s60Q=*&1hho2LLVo9^t"J(#ܓB2n;C NmbYzo}3wa;axރ峟e\,er#ܻ'\<<7<K{s=˒Nw繦sjkZl~\ a۱EOva޹V+ou"TF&ɍdRvlUfiXx0v/DfT=sBޑ&OQbf]| S8H2d-62ΉWnϳ87Z^ ȴgaB-Ho3d;;B2pj[C?=Fo-|3;{98нw58`g[W? 0b%;"ق]42/fؼ~g)&6]Tvw3-;FT0\PD3WJP^G) (UPu5PD1y )N6!ҩxapϹg/s4mzt3be|-F5a4ӿ36g&Gxv~w&p3Gfw(O=9:ȡ8[n wztX3d̨O1] d2F+E&ZP ՍY$IؽV|)? ^۠N6oX[^l*pV>\[B♅͉uڐi!X$u8+opgK} _0֯`[_vyiA)CQz!xG9}wHd]UA{46a $;޳wyKC<ו4Ǔ.KNxvG,axq l qeFФ tsΐ)wAKN"8FTk|PBVF/%ᮩ'cdD5QM&\>E y7¾}طdnol^7'9oo{KXKw`~1Gø'غ&/.:WMow-eKB|4+6+my>=PZ_#>5t<dMFyM^ֱ X4JOEe謑1j\ҽ~2 5.>@nB5 $]M#?00l30`O=9rNڊ,19"7*Ï!?t}Tgؠ.] G|}t;ob\h&cl-֚P=T<;M*5E,"ybCTi[:EfN7RFHkX=%K$ܘȡUn(BYȑ[T{\EZY)$h'H@ӵb$t 2bxK:j8q CvKx3awuMG;5>I1=x2pwXm?_-qr,ᥓ99b*rcMFQ:EN/1oU c/,(W0崤@j,"oOY2utIfoz|w?Gn>x#tdeKPFDG[PB97eByµ)Kn`rinSO8 DKFdg/-@Hĥ <9:H2w/P2#̀1- gVJ#l!Ƞ-X)ogC}mTT5ފk”NU)"GF2opU# \dy~s}/_?S ~S6[.kGan}5kg 90_g`}7I(nL*8U$T\/[!,̌b&0%6Kj鼁(J|IHK#1a\3CUSM"QЫǂTZg6toؐ;>mjy &Ɋ,JUq!pE*6aaV:|7ozh/ c8w ?OLw/sOw;*M:N(s}?2Hڮڕ~)NM^]k,AԬ =%r23`C7%eiNkX[xGc^7BS0+V4N Chq8HL}Y&WU_G,;S H(eNܢ`"cQR;8mU{'6k7m_]rak/| O?q]x=KKl- ވ'{qO~Gdy/8e2t!\a7SȘl~[蕇g 0 iTHkƷF FSq4,/뤹ArLFgFX6BrKDQ8ƈϕxu9Ae!y(uF6( PUa[/ϱϨ?Wn=Tdq^w6zr]TQ`JRHSg,6??u q-)B:mD!z.%M͉Νu(C$ HEkq:yŜbڼ tUI-x|iHUG!h Ɗ#Yӓ֤?|߿峛OO'7G8aܽǸu~yp@y[БcdA:yi Z ^"EZթ!^~D4Iĺ)kmtmAʿ֠hK*2v߳)\?ҕTʬܴ {˸Tn:QFcmtk՞N$ϦԸvP0T l7W?c&og{}#9}i[>0c_0*@M"9l$.RW ȓp%2eKj[ƔRypO=0;8q=_|,wg6X Kk {يūƂ7!R )JDŦr0)nGgv +є3 #CBkT!ך^$Q'a?4aA@읚D/)ixr0H܃\JXZðC0FU;>s-$dM_[Y$#4O2K7ML Q5=˹ow'>MmS;;pjuwnᯍd98yr5|pg2%FMiԋ%l)F+Ѹy{pFZTÚb%G LaNjKQov։3,_  A+µWC: .PGC*IW.@i;ǁ7;Y~7TNE}"nsHs%?ox_=ߦ:3bbfVjGtF~hg)}RG0 \2!ɷHT Gt~xy}mO&x NZ+G#d󽷼?eE8?Ƽ E6CwNp?F?دqc5z/mҽWq -B2!ZBG'fYy:G^jZ4$# D.L$H )W KK|^<: iM<;BLB!3FAQB5Um1C Qy*WgϾwtvw*ZWY\=`&_4jHAGCQO~ 7"/}KCFa}o.*pg-Ùi &Xֆ/ѽ8ܑuGpvkvVeL+%5{pLsOG0E+Vsе?2S5EAΓIPth"RZAT6u>HD `K0ՊhhDMPzѯ]2> ,CA0pa3Qh xo6t;MiA59(Eت9ߎ!G}vj`Mؠˉ6nG>׷^pLl9`x)~=l|4q \V|s#lJ't3<ȋ(!JH x/Y§NpuϮ8Mh9xh1> |YWo+ )p捆.i;#B`q:FkU+:@krB@NU?Cu]uENҍ֌&!ZպuӞK0s_bL *XB6~_@DqeWߴߧ..ցˈ.ug< ?7,/-9cDkdL7? +]Wbz."k+luW,+ =@6? կ`VfO>'i0[t}}$:dcF1UD9ڜ I&sF|^__҉RM͟ C`7}ٻg9뵄!}%Z[;{¯+Wxu}]_u}]_u}]__M%ǠIENDB`dms-1.5.0/dlna/000077500000000000000000000000001426116762300132265ustar00rootroot00000000000000dms-1.5.0/dlna/dlna.go000066400000000000000000000041631426116762300144770ustar00rootroot00000000000000package dlna import ( "fmt" "strings" "time" ) const ( TimeSeekRangeDomain = "TimeSeekRange.dlna.org" ContentFeaturesDomain = "contentFeatures.dlna.org" TransferModeDomain = "transferMode.dlna.org" ) type ContentFeatures struct { ProfileName string SupportTimeSeek bool SupportRange bool // Play speeds, DLNA.ORG_PS would go here if supported. Transcoded bool } func BinaryInt(b bool) uint { if b { return 1 } else { return 0 } } // flags are in hex. trailing 24 zeroes, 26 are after the space // "DLNA.ORG_OP=" time-seek-range-supp bytes-range-header-supp func (cf ContentFeatures) String() (ret string) { // DLNA.ORG_PN=[a-zA-Z0-9_]* params := make([]string, 0, 3) if cf.ProfileName != "" { params = append(params, "DLNA.ORG_PN="+cf.ProfileName) } params = append(params, fmt.Sprintf( "DLNA.ORG_OP=%b%b;DLNA.ORG_CI=%b", BinaryInt(cf.SupportTimeSeek), BinaryInt(cf.SupportRange), BinaryInt(cf.Transcoded))) params = append(params, "DLNA.ORG_FLAGS=01700000000000000000000000000000") return strings.Join(params, ";") } func ParseNPTTime(s string) (time.Duration, error) { var h, m, sec, ms time.Duration n, err := fmt.Sscanf(s, "%d:%2d:%2d.%3d", &h, &m, &sec, &ms) if err != nil { return -1, err } if n < 3 { return -1, fmt.Errorf("invalid npt time: %s", s) } ret := time.Duration(h) * time.Hour ret += time.Duration(m) * time.Minute ret += sec * time.Second ret += ms * time.Millisecond return ret, nil } func FormatNPTTime(npt time.Duration) string { npt /= time.Millisecond ms := npt % 1000 npt /= 1000 s := npt % 60 npt /= 60 m := npt % 60 npt /= 60 h := npt return fmt.Sprintf("%02d:%02d:%02d.%03d", h, m, s, ms) } type NPTRange struct { Start, End time.Duration } func ParseNPTRange(s string) (ret NPTRange, err error) { ss := strings.SplitN(s, "-", 2) if ss[0] != "" { ret.Start, err = ParseNPTTime(ss[0]) if err != nil { return } } if ss[1] != "" { ret.End, err = ParseNPTTime(ss[1]) if err != nil { return } } return } func (me NPTRange) String() (ret string) { ret = me.Start.String() + "-" if me.End >= 0 { ret += me.End.String() } return } dms-1.5.0/dlna/dlna_test.go000066400000000000000000000004351426116762300155340ustar00rootroot00000000000000package dlna import ( "testing" ) func TestContentFeaturesString(t *testing.T) { a := ContentFeatures{ Transcoded: true, SupportTimeSeek: true, }.String() e := "DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000" if e != a { t.Fatal(a) } } dms-1.5.0/dlna/dms/000077500000000000000000000000001426116762300140115ustar00rootroot00000000000000dms-1.5.0/dlna/dms/cd-service-desc.go000066400000000000000000000343711426116762300173100ustar00rootroot00000000000000package dms const contentDirectoryServiceDescription = ` 1 0 GetSearchCapabilities SearchCaps out SearchCapabilities GetSortCapabilities SortCaps out SortCapabilities GetSortExtensionCapabilities SortExtensionCaps out SortExtensionCapabilities GetFeatureList FeatureList out FeatureList GetSystemUpdateID Id out SystemUpdateID Browse ObjectID in A_ARG_TYPE_ObjectID BrowseFlag in A_ARG_TYPE_BrowseFlag Filter in A_ARG_TYPE_Filter StartingIndex in A_ARG_TYPE_Index RequestedCount in A_ARG_TYPE_Count SortCriteria in A_ARG_TYPE_SortCriteria Result out A_ARG_TYPE_Result NumberReturned out A_ARG_TYPE_Count TotalMatches out A_ARG_TYPE_Count UpdateID out A_ARG_TYPE_UpdateID Search ContainerID in A_ARG_TYPE_ObjectID SearchCriteria in A_ARG_TYPE_SearchCriteria Filter in A_ARG_TYPE_Filter StartingIndex in A_ARG_TYPE_Index RequestedCount in A_ARG_TYPE_Count SortCriteria in A_ARG_TYPE_SortCriteria Result out A_ARG_TYPE_Result NumberReturned out A_ARG_TYPE_Count TotalMatches out A_ARG_TYPE_Count UpdateID out A_ARG_TYPE_UpdateID CreateObject ContainerID in A_ARG_TYPE_ObjectID Elements in A_ARG_TYPE_Result ObjectID out A_ARG_TYPE_ObjectID Result out A_ARG_TYPE_Result DestroyObject ObjectID in A_ARG_TYPE_ObjectID UpdateObject ObjectID in A_ARG_TYPE_ObjectID CurrentTagValue in A_ARG_TYPE_TagValueList NewTagValue in A_ARG_TYPE_TagValueList MoveObject ObjectID in A_ARG_TYPE_ObjectID NewParentID in A_ARG_TYPE_ObjectID NewObjectID out A_ARG_TYPE_ObjectID ImportResource SourceURI in A_ARG_TYPE_URI DestinationURI in A_ARG_TYPE_URI TransferID out A_ARG_TYPE_TransferID ExportResource SourceURI in A_ARG_TYPE_URI DestinationURI in A_ARG_TYPE_URI TransferID out A_ARG_TYPE_TransferID StopTransferResource TransferID in A_ARG_TYPE_TransferID DeleteResource ResourceURI in A_ARG_TYPE_URI GetTransferProgress TransferID in A_ARG_TYPE_TransferID TransferStatus out A_ARG_TYPE_TransferStatus TransferLength out A_ARG_TYPE_TransferLength TransferTotal out A_ARG_TYPE_TransferTotal CreateReference ContainerID in A_ARG_TYPE_ObjectID ObjectID in A_ARG_TYPE_ObjectID NewID out A_ARG_TYPE_ObjectID X_GetFeatureList FeatureList out A_ARG_TYPE_Featurelist X_SetBookmark CategoryType in A_ARG_TYPE_CategoryType RID in A_ARG_TYPE_RID ObjectID in A_ARG_TYPE_ObjectID PosSecond in A_ARG_TYPE_PosSec SearchCapabilities string SortCapabilities string SortExtensionCapabilities string SystemUpdateID ui4 ContainerUpdateIDs string TransferIDs string FeatureList string A_ARG_TYPE_ObjectID string A_ARG_TYPE_Result string A_ARG_TYPE_SearchCriteria string A_ARG_TYPE_BrowseFlag string BrowseMetadata BrowseDirectChildren A_ARG_TYPE_Filter string A_ARG_TYPE_SortCriteria string A_ARG_TYPE_Index ui4 A_ARG_TYPE_Count ui4 A_ARG_TYPE_UpdateID ui4 A_ARG_TYPE_TransferID ui4 A_ARG_TYPE_TransferStatus string COMPLETED ERROR IN_PROGRESS STOPPED A_ARG_TYPE_TransferLength string A_ARG_TYPE_TransferTotal string A_ARG_TYPE_TagValueList string A_ARG_TYPE_URI uri A_ARG_TYPE_CategoryType ui4 A_ARG_TYPE_RID ui4 A_ARG_TYPE_PosSec ui4 A_ARG_TYPE_Featurelist string ` dms-1.5.0/dlna/dms/cds.go000066400000000000000000000255041426116762300151170ustar00rootroot00000000000000package dms import ( "encoding/xml" "fmt" "net/http" "net/url" "os" "path" "path/filepath" "sort" "strings" "github.com/anacrolix/log" "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/misc" "github.com/anacrolix/dms/upnp" "github.com/anacrolix/dms/upnpav" "github.com/anacrolix/ffprobe" ) type contentDirectoryService struct { *Server upnp.Eventing } func (cds *contentDirectoryService) updateIDString() string { return fmt.Sprintf("%d", uint32(os.Getpid())) } // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. func (me *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host, userAgent string) (ret interface{}, err error) { entryFilePath := cdsObject.FilePath() ignored, err := me.IgnorePath(entryFilePath) if err != nil { return } if ignored { return } obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), } if fileInfo.IsDir() { obj.Class = "object.container.storageFolder" obj.Title = fileInfo.Name() ret = upnpav.Container{Object: obj, ChildCount: me.objectChildCount(cdsObject)} return } if !fileInfo.Mode().IsRegular() { me.Logger.Printf("%s ignored: non-regular file", cdsObject.FilePath()) return } mimeType, err := MimeTypeByPath(entryFilePath) if err != nil { return } if !mimeType.IsMedia() { me.Logger.Printf("%s ignored: non-media file (%s)", cdsObject.FilePath(), mimeType) return } iconURI := (&url.URL{ Scheme: "http", Host: host, Path: iconPath, RawQuery: url.Values{ "path": {cdsObject.Path}, }.Encode(), }).String() obj.Icon = iconURI // TODO(anacrolix): This might not be necessary due to item res image // element. obj.AlbumArtURI = iconURI obj.Class = "object.item." + mimeType.Type() + "Item" var ( ffInfo *ffprobe.Info nativeBitrate uint resDuration string ) if !me.NoProbe { ffInfo, probeErr := me.ffmpegProbe(entryFilePath) switch probeErr { case nil: if ffInfo != nil { nativeBitrate, _ = ffInfo.Bitrate() if d, err := ffInfo.Duration(); err == nil { resDuration = misc.FormatDurationSexagesimal(d) } } case ffprobe.ExeNotFound: default: me.Logger.Printf("error probing %s: %s", entryFilePath, probeErr) } } if obj.Title == "" { obj.Title = fileInfo.Name() } resolution := func() string { if ffInfo != nil { for _, strm := range ffInfo.Streams { if strm["codec_type"] != "video" { continue } width := strm["width"] height := strm["height"] return fmt.Sprintf("%.0fx%.0f", width, height) } } return "" }() item := upnpav.Item{ Object: obj, // Capacity: 1 for raw, 1 for icon, plus transcodes. Res: make([]upnpav.Resource, 0, 2+len(transcodes)), } item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, Path: resPath, RawQuery: url.Values{ "path": {cdsObject.Path}, }.Encode(), }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, }.String()), Bitrate: nativeBitrate, Duration: resDuration, Size: uint64(fileInfo.Size()), Resolution: resolution, }) if mimeType.IsVideo() { if !me.NoTranscode { item.Res = append(item.Res, transcodeResources(host, cdsObject.Path, resolution, resDuration)...) } } if mimeType.IsVideo() || mimeType.IsImage() { item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, Path: iconPath, RawQuery: url.Values{ "path": {cdsObject.Path}, "c": {"jpeg"}, }.Encode(), }).String(), ProtocolInfo: "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN", }) } ret = item return } // Returns all the upnpav objects in a directory. func (me *contentDirectoryService) readContainer(o object, host, userAgent string) (ret []interface{}, err error) { sfis := sortableFileInfoSlice{ // TODO(anacrolix): Dig up why this special cast was added. FoldersLast: strings.Contains(userAgent, `AwoX/1.1`), } sfis.fileInfoSlice, err = o.readDir() if err != nil { return } sort.Sort(sfis) for _, fi := range sfis.fileInfoSlice { child := object{path.Join(o.Path, fi.Name()), me.RootObjectPath} obj, err := me.cdsObjectToUpnpavObject(child, fi, host, userAgent) if err != nil { me.Logger.Printf("error with %s: %s", child.FilePath(), err) continue } if obj != nil { ret = append(ret, obj) } } return } type browse struct { ObjectID string BrowseFlag string Filter string StartingIndex int RequestedCount int } // ContentDirectory object from ObjectID. func (me *contentDirectoryService) objectFromID(id string) (o object, err error) { o.Path, err = url.QueryUnescape(id) if err != nil { return } if o.Path == "0" { o.Path = "/" } o.Path = path.Clean(o.Path) if !path.IsAbs(o.Path) { err = fmt.Errorf("bad ObjectID %v", o.Path) return } o.RootObjectPath = me.RootObjectPath return } func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) ([][2]string, error) { host := r.Host userAgent := r.UserAgent() switch action { case "GetSystemUpdateID": return [][2]string{ {"Id", me.updateIDString()}, }, nil case "GetSortCapabilities": return [][2]string{ {"SortCaps", "dc:title"}, }, nil case "Browse": var browse browse if err := xml.Unmarshal([]byte(argsXML), &browse); err != nil { return nil, err } obj, err := me.objectFromID(browse.ObjectID) if err != nil { return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error()) } switch browse.BrowseFlag { case "BrowseDirectChildren": var objs []interface{} if me.OnBrowseDirectChildren == nil { objs, err = me.readContainer(obj, host, userAgent) } else { objs, err = me.OnBrowseDirectChildren(obj.Path, obj.RootObjectPath, host, userAgent) } if err != nil { return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error()) } totalMatches := len(objs) objs = objs[func() (low int) { low = browse.StartingIndex if low > len(objs) { low = len(objs) } return }():] if browse.RequestedCount != 0 && int(browse.RequestedCount) < len(objs) { objs = objs[:browse.RequestedCount] } result, err := xml.Marshal(objs) if err != nil { return nil, err } return [][2]string{ {"Result", didl_lite(string(result))}, {"NumberReturned", fmt.Sprint(len(objs))}, {"TotalMatches", fmt.Sprint(totalMatches)}, {"UpdateID", me.updateIDString()}, }, nil case "BrowseMetadata": var ret interface{} var err error if me.OnBrowseMetadata == nil { var fileInfo os.FileInfo fileInfo, err = os.Stat(obj.FilePath()) if err != nil { if os.IsNotExist(err) { return nil, &upnp.Error{ Code: upnpav.NoSuchObjectErrorCode, Desc: err.Error(), } } return nil, err } ret, err = me.cdsObjectToUpnpavObject(obj, fileInfo, host, userAgent) } else { ret, err = me.OnBrowseMetadata(obj.Path, obj.RootObjectPath, host, userAgent) } if err != nil { return nil, err } buf, err := xml.Marshal(ret) if err != nil { return nil, err } return [][2]string{ {"Result", didl_lite(func() string { return string(buf) }())}, {"NumberReturned", "1"}, {"TotalMatches", "1"}, {"UpdateID", me.updateIDString()}, }, nil default: return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag) } case "GetSearchCapabilities": return [][2]string{ {"SearchCaps", ""}, }, nil // Samsung Extensions case "X_GetFeatureList": // TODO: make it dependable on model // https://github.com/1100101/minidlna/blob/ca6dbba18390ad6f8b8d7b7dbcf797dbfd95e2db/upnpsoap.c#L2153-L2199 return [][2]string{ {"FeatureList", ` // "A" // "V" // "I" `}, }, nil case "X_SetBookmark": // just ignore return [][2]string{}, nil default: return nil, upnp.InvalidActionError } } // Represents a ContentDirectory object. type object struct { Path string // The cleaned, absolute path for the object relative to the server. RootObjectPath string } // Returns the number of children this object has, such as for a container. func (cds *contentDirectoryService) objectChildCount(me object) int { objs, err := cds.readContainer(me, "", "") if err != nil { cds.Logger.Printf("error reading container: %s", err) } return len(objs) } func (cds *contentDirectoryService) objectHasChildren(obj object) bool { return cds.objectChildCount(obj) != 0 } // Returns the actual local filesystem path for the object. func (o *object) FilePath() string { return filepath.Join(o.RootObjectPath, filepath.FromSlash(o.Path)) } // Returns the ObjectID for the object. This is used in various ContentDirectory actions. func (o object) ID() string { if !path.IsAbs(o.Path) { log.Panicf("Relative object path: %s", o.Path) } if len(o.Path) == 1 { return "0" } return url.QueryEscape(o.Path) } func (o *object) IsRoot() bool { return o.Path == "/" } // Returns the object's parent ObjectID. Fortunately it can be deduced from the // ObjectID (for now). func (o object) ParentID() string { if o.IsRoot() { return "-1" } o.Path = path.Dir(o.Path) return o.ID() } // This function exists rather than just calling os.(*File).Readdir because I // want to stat(), not lstat() each entry. func (o *object) readDir() (fis []os.FileInfo, err error) { dirPath := o.FilePath() dirFile, err := os.Open(dirPath) if err != nil { return } defer dirFile.Close() var dirContent []string dirContent, err = dirFile.Readdirnames(-1) if err != nil { return } fis = make([]os.FileInfo, 0, len(dirContent)) for _, file := range dirContent { fi, err := os.Stat(filepath.Join(dirPath, file)) if err != nil { continue } fis = append(fis, fi) } return } type sortableFileInfoSlice struct { fileInfoSlice []os.FileInfo FoldersLast bool } func (me sortableFileInfoSlice) Len() int { return len(me.fileInfoSlice) } func (me sortableFileInfoSlice) Less(i, j int) bool { if me.fileInfoSlice[i].IsDir() && !me.fileInfoSlice[j].IsDir() { return !me.FoldersLast } if !me.fileInfoSlice[i].IsDir() && me.fileInfoSlice[j].IsDir() { return me.FoldersLast } return strings.ToLower(me.fileInfoSlice[i].Name()) < strings.ToLower(me.fileInfoSlice[j].Name()) } func (me sortableFileInfoSlice) Swap(i, j int) { me.fileInfoSlice[i], me.fileInfoSlice[j] = me.fileInfoSlice[j], me.fileInfoSlice[i] } dms-1.5.0/dlna/dms/cds_test.go000066400000000000000000000007041426116762300161510ustar00rootroot00000000000000package dms import ( "strings" "testing" ) func TestEscapeObjectID(t *testing.T) { o := object{ Path: "/some/file", } id := o.ID() if strings.ContainsAny(id, "/") { t.Skip("may not work with some players: object IDs contain '/'") } } func TestRootObjectID(t *testing.T) { if (object{Path: "/"}).ID() != "0" { t.FailNow() } } func TestRootParentObjectID(t *testing.T) { if (object{Path: "/"}).ParentID() != "-1" { t.FailNow() } } dms-1.5.0/dlna/dms/cm-service-desc.go000066400000000000000000000126551426116762300173220ustar00rootroot00000000000000package dms const connectionManagerServiceDescription = ` 1 0 GetProtocolInfo Source out SourceProtocolInfo Sink out SinkProtocolInfo PrepareForConnection RemoteProtocolInfo in A_ARG_TYPE_ProtocolInfo PeerConnectionManager in A_ARG_TYPE_ConnectionManager PeerConnectionID in A_ARG_TYPE_ConnectionID Direction in A_ARG_TYPE_Direction ConnectionID out A_ARG_TYPE_ConnectionID AVTransportID out A_ARG_TYPE_AVTransportID RcsID out A_ARG_TYPE_RcsID ConnectionComplete ConnectionID in A_ARG_TYPE_ConnectionID GetCurrentConnectionIDs ConnectionIDs out CurrentConnectionIDs GetCurrentConnectionInfo ConnectionID in A_ARG_TYPE_ConnectionID RcsID out A_ARG_TYPE_RcsID AVTransportID out A_ARG_TYPE_AVTransportID ProtocolInfo out A_ARG_TYPE_ProtocolInfo PeerConnectionManager out A_ARG_TYPE_ConnectionManager PeerConnectionID out A_ARG_TYPE_ConnectionID Direction out A_ARG_TYPE_Direction Status out A_ARG_TYPE_ConnectionStatus SourceProtocolInfo string SinkProtocolInfo string CurrentConnectionIDs string A_ARG_TYPE_ConnectionStatus string OK ContentFormatMismatch InsufficientBandwidth UnreliableChannel Unknown A_ARG_TYPE_ConnectionManager string A_ARG_TYPE_Direction string Input Output A_ARG_TYPE_ProtocolInfo string A_ARG_TYPE_ConnectionID i4 A_ARG_TYPE_AVTransportID i4 A_ARG_TYPE_RcsID i4 ` dms-1.5.0/dlna/dms/cms.go000066400000000000000000000070241426116762300151250ustar00rootroot00000000000000package dms import ( "net/http" "github.com/anacrolix/dms/upnp" ) // const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" const defaultProtocolInfo = "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_RES_H_V,http-get:*:image/png:DLNA.ORG_PN=PNG_TN,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3,http-get:*:audio/L16:DLNA.ORG_PN=LPCM,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_24_AC3_ISO;SONY.COM_PN=AVC_TS_HD_24_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_24_AC3;SONY.COM_PN=AVC_TS_HD_24_AC3,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_24_AC3_T;SONY.COM_PN=AVC_TS_HD_24_AC3_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_PS_PAL,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_PS_NTSC,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_50_L2_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_60_L2_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_50_AC3_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_HD_50_L2_ISO;SONY.COM_PN=HD2_50_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_60_AC3_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_HD_60_L2_ISO;SONY.COM_PN=HD2_60_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_HD_50_L2_T;SONY.COM_PN=HD2_50_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_HD_60_L2_T;SONY.COM_PN=HD2_60_T,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_50_AC3_ISO;SONY.COM_PN=AVC_TS_HD_50_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_50_AC3;SONY.COM_PN=AVC_TS_HD_50_AC3,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_60_AC3_ISO;SONY.COM_PN=AVC_TS_HD_60_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_60_AC3;SONY.COM_PN=AVC_TS_HD_60_AC3,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_50_AC3_T;SONY.COM_PN=AVC_TS_HD_50_AC3_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_60_AC3_T;SONY.COM_PN=AVC_TS_HD_60_AC3_T,http-get:*:video/x-mp2t-mphl-188:*,http-get:*:video/*:*,http-get:*:audio/*:*,http-get:*:image/*:*,http-get:*:text/srt:*,http-get:*:text/smi:*,http-get:*:text/ssa:*,http-get:*:*:*" type connectionManagerService struct { *Server upnp.Eventing } func (cms *connectionManagerService) Handle(action string, argsXML []byte, r *http.Request) ([][2]string, error) { switch action { case ".GetCurrentConnectionInfo": return [][2]string{ {"ConnectionID", "0"}, {"RcsID", "-1"}, {"AVTransportID", "-1"}, {"ProtocolInfo", ""}, {"PeerConnectionManager", ""}, {"PeerConnectionID", "-1"}, {"Direction", "Output"}, {"Status", "OK"}, }, nil case "GetCurrentConnectionIDs": return [][2]string{ {"ConnectionIDs", ""}, }, nil case "GetProtocolInfo": return [][2]string{ {"Source", defaultProtocolInfo}, {"Sink", ""}, }, nil default: return nil, upnp.InvalidActionError } } dms-1.5.0/dlna/dms/dms.go000066400000000000000000000713131426116762300151300ustar00rootroot00000000000000package dms import ( "bytes" "crypto/md5" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/pprof" "net/url" "os" "os/exec" "os/user" "path" "path/filepath" "strconv" "strings" "sync" "time" "github.com/anacrolix/log" "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/ssdp" "github.com/anacrolix/dms/transcode" "github.com/anacrolix/dms/upnp" "github.com/anacrolix/dms/upnpav" "github.com/anacrolix/dms/version" "github.com/anacrolix/ffprobe" ) var ( serverField = fmt.Sprintf(`Linux/3.4 DLNADOC/1.50 UPnP/1.0 %s/%s`, userAgentProduct, version.DmsVersion) rootDeviceModelName = fmt.Sprintf("%s %s", userAgentProduct, version.DmsVersion) ) const ( userAgentProduct = "dms" rootDeviceType = "urn:schemas-upnp-org:device:MediaServer:1" resPath = "/res" iconPath = "/icon" rootDescPath = "/rootDesc.xml" contentDirectoryEventSubURL = "/evt/ContentDirectory" serviceControlURL = "/ctl" deviceIconPath = "/deviceIcon" ) type transcodeSpec struct { mimeType string DLNAProfileName string Transcode func(path string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) } var transcodes = map[string]transcodeSpec{ "t": { mimeType: "video/mpeg", DLNAProfileName: "MPEG_PS_PAL", Transcode: transcode.Transcode, }, "vp8": {mimeType: "video/webm", Transcode: transcode.VP8Transcode}, "chromecast": {mimeType: "video/mp4", Transcode: transcode.ChromecastTranscode}, "web": {mimeType: "video/mp4", Transcode: transcode.WebTranscode}, } func makeDeviceUuid(unique string) string { h := md5.New() if _, err := io.WriteString(h, unique); err != nil { log.Panicf("makeDeviceUuid write failed: %s", err) } buf := h.Sum(nil) return upnp.FormatUUID(buf) } // Groups the service definition with its XML description. type service struct { upnp.Service SCPD string } // Exposed UPnP AV services. var services = []*service{ { Service: upnp.Service{ ServiceType: "urn:schemas-upnp-org:service:ContentDirectory:1", ServiceId: "urn:upnp-org:serviceId:ContentDirectory", EventSubURL: contentDirectoryEventSubURL, }, SCPD: contentDirectoryServiceDescription, }, { Service: upnp.Service{ ServiceType: "urn:schemas-upnp-org:service:ConnectionManager:1", ServiceId: "urn:upnp-org:serviceId:ConnectionManager", }, SCPD: connectionManagerServiceDescription, }, { Service: upnp.Service{ ServiceType: "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", ServiceId: "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar", }, SCPD: mediaReceiverRegistrarDescription, }, } // The control URL for every service is the same. We're able to infer the desired service from the request headers. func init() { for _, s := range services { s.ControlURL = serviceControlURL } } func devices() []string { return []string{ "urn:schemas-upnp-org:device:MediaServer:1", } } func serviceTypes() (ret []string) { for _, s := range services { ret = append(ret, s.ServiceType) } return } func (me *Server) httpPort() int { return me.HTTPConn.Addr().(*net.TCPAddr).Port } func (me *Server) serveHTTP() error { srv := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if me.LogHeaders { fmt.Fprintf(os.Stderr, "%s %s\r\n", r.Method, r.RequestURI) r.Header.Write(os.Stderr) fmt.Fprintln(os.Stderr) } w.Header().Set("Ext", "") w.Header().Set("Server", serverField) me.httpServeMux.ServeHTTP(&mitmRespWriter{ ResponseWriter: w, logHeader: me.LogHeaders, }, r) }), } err := srv.Serve(me.HTTPConn) select { case <-me.closed: return nil default: return err } } // An interface with these flags should be valid for SSDP. const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast func (me *Server) doSSDP() { var wg sync.WaitGroup for _, if_ := range me.Interfaces { if_ := if_ wg.Add(1) go func() { defer wg.Done() me.ssdpInterface(if_) }() } wg.Wait() } // Run SSDP server on an interface. func (me *Server) ssdpInterface(if_ net.Interface) { logger := me.Logger.WithNames("ssdp", if_.Name) s := ssdp.Server{ Interface: if_, Devices: devices(), Services: serviceTypes(), Location: func(ip net.IP) string { return me.location(ip) }, Server: serverField, UUID: me.rootDeviceUUID, NotifyInterval: me.NotifyInterval, Logger: logger, } if err := s.Init(); err != nil { if if_.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags { // Didn't expect it to work anyway. return } if strings.Contains(err.Error(), "listen") { // OSX has a lot of dud interfaces. Failure to create a socket on // the interface are what we're expecting if the interface is no // good. return } logger.Printf("error creating ssdp server on %s: %s", if_.Name, err) return } defer s.Close() logger.Levelf(log.Info, "started SSDP on %q", if_.Name) stopped := make(chan struct{}) go func() { defer close(stopped) if err := s.Serve(); err != nil { logger.Printf("%q: %q\n", if_.Name, err) } }() select { case <-me.closed: // Returning will close the server. case <-stopped: } } var startTime time.Time type Icon struct { Width, Height, Depth int Mimetype string Bytes []byte } type Server struct { HTTPConn net.Listener FriendlyName string Interfaces []net.Interface httpServeMux *http.ServeMux RootObjectPath string OnBrowseDirectChildren func(path string, rootObjectPath string, host, userAgent string) (ret []interface{}, err error) OnBrowseMetadata func(path string, rootObjectPath string, host, userAgent string) (ret interface{}, err error) rootDescXML []byte rootDeviceUUID string FFProbeCache Cache closed chan struct{} ssdpStopped chan struct{} // The service SOAP handler keyed by service URN. services map[string]UPnPService LogHeaders bool // Disable transcoding, and the resource elements implied in the CDS. NoTranscode bool // Force transcoding to certain format of the 'transcodes' map ForceTranscodeTo string // Disable media probing with ffprobe NoProbe bool Icons []Icon // Stall event subscription requests until they drop. A workaround for // some bad clients. StallEventSubscribe bool // Time interval between SSPD announces NotifyInterval time.Duration // Ignore hidden files and directories IgnoreHidden bool // Ingnore unreadable files and directories IgnoreUnreadable bool // White list of clients AllowedIpNets []*net.IPNet Logger log.Logger eventingLogger log.Logger } // UPnP SOAP service. type UPnPService interface { Handle(action string, argsXML []byte, r *http.Request) (respArgs [][2]string, err error) Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error) Unsubscribe(sid string) error } type Cache interface { Set(key interface{}, value interface{}) Get(key interface{}) (value interface{}, ok bool) } type dummyFFProbeCache struct{} func (dummyFFProbeCache) Set(interface{}, interface{}) {} func (dummyFFProbeCache) Get(interface{}) (interface{}, bool) { return nil, false } // Public definition so that external modules can persist cache contents. type FfprobeCacheItem struct { Key ffmpegInfoCacheKey Value *ffprobe.Info } // update the UPnP object fields from ffprobe data // priority is given the format section, and then the streams sequentially func itemExtra(item *upnpav.Object, info *ffprobe.Info) { setFromTags := func(m map[string]interface{}) { for key, val := range m { setIfUnset := func(s *string) { if *s == "" { *s = val.(string) } } switch strings.ToLower(key) { case "tag:artist": setIfUnset(&item.Artist) case "tag:album": setIfUnset(&item.Album) case "tag:genre": setIfUnset(&item.Genre) } } } setFromTags(info.Format) for _, m := range info.Streams { setFromTags(m) } } type ffmpegInfoCacheKey struct { Path string ModTime int64 } func transcodeResources(host, path, resolution, duration string) (ret []upnpav.Resource) { ret = make([]upnpav.Resource, 0, len(transcodes)) for k, v := range transcodes { ret = append(ret, upnpav.Resource{ ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", v.mimeType, dlna.ContentFeatures{ SupportTimeSeek: true, Transcoded: true, ProfileName: v.DLNAProfileName, }.String()), URL: (&url.URL{ Scheme: "http", Host: host, Path: resPath, RawQuery: url.Values{ "path": {path}, "transcode": {k}, }.Encode(), }).String(), Resolution: resolution, Duration: duration, }) } return } func parseDLNARangeHeader(val string) (ret dlna.NPTRange, err error) { if !strings.HasPrefix(val, "npt=") { err = errors.New("bad prefix") return } ret, err = dlna.ParseNPTRange(val[len("npt="):]) if err != nil { return } return } // Determines the time-based range to transcode, and sets the appropriate // headers. Returns !ok if there was an error and the caller should stop // handling the request. func handleDLNARange(w http.ResponseWriter, hs http.Header) (r dlna.NPTRange, partialResponse, ok bool) { if len(hs[http.CanonicalHeaderKey(dlna.TimeSeekRangeDomain)]) == 0 { ok = true return } partialResponse = true h := hs.Get(dlna.TimeSeekRangeDomain) r, err := parseDLNARangeHeader(h) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Passing an exact NPT duration seems to cause trouble pass the "iono" // (*) duration instead. // // TODO: Check that the request range can't already have /. w.Header().Set(dlna.TimeSeekRangeDomain, h+"/*") ok = true return } func (me *Server) serveDLNATranscode(w http.ResponseWriter, r *http.Request, path_ string, ts transcodeSpec, tsname string) { w.Header().Set(dlna.TransferModeDomain, "Streaming") w.Header().Set("content-type", ts.mimeType) w.Header().Set(dlna.ContentFeaturesDomain, (dlna.ContentFeatures{ Transcoded: true, SupportTimeSeek: true, }).String()) // If a range of any kind is given, we have to respond with 206 if we're // interpreting that range. Since only the DLNA range is handled in this // function, it alone determines if we'll give a partial response. range_, partialResponse, ok := handleDLNARange(w, r.Header) if !ok { return } ffInfo, _ := me.ffmpegProbe(path_) if ffInfo != nil { if duration, err := ffInfo.Duration(); err == nil { s := fmt.Sprintf("%f", duration.Seconds()) w.Header().Set("content-duration", s) w.Header().Set("x-content-duration", s) } } stderrPath := func() string { u, _ := user.Current() return filepath.Join(u.HomeDir, ".dms", "log", tsname, filepath.Base(path_)) }() os.MkdirAll(filepath.Dir(stderrPath), 0o750) logFile, err := os.Create(stderrPath) if err != nil { log.Printf("couldn't create transcode log file: %s", err) } else { defer logFile.Close() log.Printf("logging transcode to %q", stderrPath) } p, err := ts.Transcode(path_, range_.Start, range_.End-range_.Start, logFile) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer p.Close() // I recently switched this to returning 200 if no range is specified for // pure UPnP clients. It's possible that DLNA clients will *always* expect // 206. It appears the HTTP standard requires that 206 only be used if a // response is not interpreting any range headers. w.WriteHeader(func() int { if partialResponse { return http.StatusPartialContent } else { return http.StatusOK } }()) io.Copy(w, p) } func init() { startTime = time.Now() } func getDefaultFriendlyName() string { return fmt.Sprintf("%s: %s on %s", rootDeviceModelName, func() string { user, err := user.Current() if err != nil { log.Panicf("getDefaultFriendlyName could not get username: %s", err) } return user.Name }(), func() string { name, err := os.Hostname() if err != nil { log.Panicf("getDefaultFriendlyName could not get hostname: %s", err) } return name }()) } func xmlMarshalOrPanic(value interface{}) []byte { ret, err := xml.MarshalIndent(value, "", " ") if err != nil { log.Panicf("xmlMarshalOrPanic failed to marshal %v: %s", value, err) } return ret } // TODO: Document the use of this for debugging. type mitmRespWriter struct { http.ResponseWriter loggedHeader bool logHeader bool } func (me *mitmRespWriter) WriteHeader(code int) { me.doLogHeader(code) me.ResponseWriter.WriteHeader(code) } func (me *mitmRespWriter) doLogHeader(code int) { if !me.logHeader { return } fmt.Fprintln(os.Stderr, code) for k, v := range me.Header() { fmt.Fprintln(os.Stderr, k, v) } fmt.Fprintln(os.Stderr) me.loggedHeader = true } func (me *mitmRespWriter) Write(b []byte) (int, error) { if !me.loggedHeader { me.doLogHeader(200) } return me.ResponseWriter.Write(b) } func (me *mitmRespWriter) CloseNotify() <-chan bool { return me.ResponseWriter.(http.CloseNotifier).CloseNotify() } // Set the SCPD serve paths. func init() { for _, s := range services { lastInd := strings.LastIndex(s.ServiceId, ":") p := path.Join("/scpd", s.ServiceId[lastInd+1:]) s.SCPDURL = p + ".xml" } } // Install handlers to serve SCPD for each UPnP service. func handleSCPDs(mux *http.ServeMux) { for _, s := range services { mux.HandleFunc(s.SCPDURL, func(serviceDesc string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", `text/xml; charset="utf-8"`) http.ServeContent(w, r, "", startTime, bytes.NewReader([]byte(serviceDesc))) } }(s.SCPD)) } } // Marshal SOAP response arguments into a response XML snippet. func marshalSOAPResponse(sa upnp.SoapAction, args [][2]string) []byte { soapArgs := make([]soap.Arg, 0, len(args)) for _, arg := range args { argName, value := arg[0], arg[1] soapArgs = append(soapArgs, soap.Arg{ XMLName: xml.Name{Local: argName}, Value: value, }) } return []byte(fmt.Sprintf(`%[3]s`, sa.Action, sa.ServiceURN.String(), xmlMarshalOrPanic(soapArgs))) } // Handle a SOAP request and return the response arguments or UPnP error. func (me *Server) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) ([][2]string, error) { service, ok := me.services[sa.Type] if !ok { // TODO: What's the invalid service error?! return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type) } return service.Handle(sa.Action, actionRequestXML, r) } // Handle a service control HTTP request. func (me *Server) serviceControlHandler(w http.ResponseWriter, r *http.Request) { found := false clientIp, _, _ := net.SplitHostPort(r.RemoteAddr) if zoneDelimiterIdx := strings.Index(clientIp, "%"); zoneDelimiterIdx != -1 { // IPv6 addresses may have the form address%zone (e.g. ::1%eth0) clientIp = clientIp[:zoneDelimiterIdx] } for _, ipnet := range me.AllowedIpNets { if ipnet.Contains(net.ParseIP(clientIp)) { found = true } } if !found { log.Printf("not allowed client %s, %+v", clientIp, me.AllowedIpNets) http.Error(w, "forbidden", http.StatusForbidden) return } soapActionString := r.Header.Get("SOAPACTION") soapAction, err := upnp.ParseActionHTTPHeader(soapActionString) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var env soap.Envelope if err := xml.NewDecoder(r.Body).Decode(&env); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // AwoX/1.1 UPnP/1.0 DLNADOC/1.50 // log.Println(r.UserAgent()) w.Header().Set("Content-Type", `text/xml; charset="utf-8"`) w.Header().Set("Ext", "") w.Header().Set("Server", serverField) soapRespXML, code := func() ([]byte, int) { respArgs, err := me.soapActionResponse(soapAction, env.Body.Action, r) if err != nil { upnpErr := upnp.ConvertError(err) return xmlMarshalOrPanic(soap.NewFault("UPnPError", upnpErr)), 500 } return marshalSOAPResponse(soapAction, respArgs), 200 }() bodyStr := fmt.Sprintf(`%s`, soapRespXML) w.WriteHeader(code) if _, err := w.Write([]byte(bodyStr)); err != nil { log.Print(err) } } func safeFilePath(root, given string) string { return filepath.Join(root, filepath.FromSlash(path.Clean("/" + given))[1:]) } func (s *Server) filePath(_path string) string { return safeFilePath(s.RootObjectPath, _path) } func (me *Server) serveIcon(w http.ResponseWriter, r *http.Request) { filePath := me.filePath(r.URL.Query().Get("path")) c := r.URL.Query().Get("c") if c == "" { c = "png" } cmd := exec.Command("ffmpegthumbnailer", "-i", filePath, "-o", "/dev/stdout", "-c"+c) // cmd.Stderr = os.Stderr body, err := cmd.Output() if err != nil { // serve 1st Icon if no ffmpegthumbnailer w.Header().Set("Content-Type", me.Icons[0].Mimetype) http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(me.Icons[0].Bytes)) // http.Error(w, err.Error(), http.StatusInternalServerError) return } http.ServeContent(w, r, "", time.Now(), bytes.NewReader(body)) } func (server *Server) contentDirectoryInitialEvent(urls []*url.URL, sid string) { body := xmlMarshalOrPanic(upnp.PropertySet{ Properties: []upnp.Property{ { Variable: upnp.Variable{ XMLName: xml.Name{ Local: "SystemUpdateID", }, Value: "0", }, }, // upnp.Property{ // Variable: upnp.Variable{ // XMLName: xml.Name{ // Local: "ContainerUpdateIDs", // }, // }, // }, // upnp.Property{ // Variable: upnp.Variable{ // XMLName: xml.Name{ // Local: "TransferIDs", // }, // }, // }, }, Space: "urn:schemas-upnp-org:event-1-0", }) body = append([]byte(``+"\n"), body...) server.eventingLogger.Print(string(body)) for _, _url := range urls { bodyReader := bytes.NewReader(body) req, err := http.NewRequest("NOTIFY", _url.String(), bodyReader) if err != nil { log.Printf("Could not create a request to notify %s: %s", _url.String(), err) continue } req.Header["CONTENT-TYPE"] = []string{`text/xml; charset="utf-8"`} req.Header["NT"] = []string{"upnp:event"} req.Header["NTS"] = []string{"upnp:propchange"} req.Header["SID"] = []string{sid} req.Header["SEQ"] = []string{"0"} // req.Header["TRANSFER-ENCODING"] = []string{"chunked"} // req.ContentLength = int64(bodyReader.Len()) server.eventingLogger.Print(req.Header) server.eventingLogger.Print("starting notify") resp, err := http.DefaultClient.Do(req) server.eventingLogger.Print("finished notify") if err != nil { log.Printf("Could not notify %s: %s", _url.String(), err) continue } server.eventingLogger.Print(resp) b, _ := ioutil.ReadAll(resp.Body) server.eventingLogger.Println(string(b)) resp.Body.Close() } } func (server *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http.Request) { if server.StallEventSubscribe { // I have an LG TV that doesn't like my eventing implementation. // Returning unimplemented (501?) errors, results in repeat subscribe // attempts which hits some kind of error count limit on the TV // causing it to forcefully disconnect. It also won't work if the CDS // service doesn't include an EventSubURL. The best thing I can do is // cause every attempt to subscribe to timeout on the TV end, which // reduces the error rate enough that the TV continues to operate // without eventing. // // I've not found a reliable way to identify this TV, since it and // others don't seem to include any client-identifying headers on // SUBSCRIBE requests. // // TODO: Get eventing to work with the problematic TV. t := time.Now() <-w.(http.CloseNotifier).CloseNotify() server.eventingLogger.Printf("stalled subscribe connection went away after %s", time.Since(t)) return } // The following code is a work in progress. It partially implements // the spec on eventing but hasn't been completed as I have nothing to // test it with. server.eventingLogger.Print(r.Header) service := server.services["ContentDirectory"] server.eventingLogger.Println(r.RemoteAddr, r.Method, r.Header.Get("SID")) if r.Method == "SUBSCRIBE" && r.Header.Get("SID") == "" { urls := upnp.ParseCallbackURLs(r.Header.Get("CALLBACK")) server.eventingLogger.Println(urls) var timeout int fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &timeout) server.eventingLogger.Println(timeout, r.Header.Get("TIMEOUT")) sid, timeout, _ := service.Subscribe(urls, timeout) w.Header()["SID"] = []string{sid} w.Header()["TIMEOUT"] = []string{fmt.Sprintf("Second-%d", timeout)} // TODO: Shouldn't have to do this to get headers logged. w.WriteHeader(http.StatusOK) go func() { time.Sleep(100 * time.Millisecond) server.contentDirectoryInitialEvent(urls, sid) }() } else if r.Method == "SUBSCRIBE" { http.Error(w, "meh", http.StatusPreconditionFailed) } else { server.eventingLogger.Printf("unhandled event method: %s", r.Method) } } func (server *Server) initMux(mux *http.ServeMux) { // Handle root (presentationURL) mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { resp.Header().Set("content-type", "text/html") err := rootTmpl.Execute(resp, struct { Readonly bool Path string }{ true, server.RootObjectPath, }) if err != nil { log.Println(err) } }) mux.HandleFunc(contentDirectoryEventSubURL, server.contentDirectoryEventSubHandler) mux.HandleFunc(iconPath, server.serveIcon) mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) { filePath := server.filePath(r.URL.Query().Get("path")) if ignored, err := server.IgnorePath(filePath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } else if ignored { http.Error(w, "no such object", http.StatusNotFound) return } var k string if server.ForceTranscodeTo != "" { k = server.ForceTranscodeTo } else { k = r.URL.Query().Get("transcode") } mimeType, err := MimeTypeByPath(filePath) if k == "" || mimeType.IsImage() { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", string(mimeType)) http.ServeFile(w, r, filePath) return } if server.NoTranscode { http.Error(w, "transcodes disabled", http.StatusNotFound) return } spec, ok := transcodes[k] if !ok { http.Error(w, fmt.Sprintf("bad transcode spec key: %s", k), http.StatusBadRequest) return } server.serveDLNATranscode(w, r, filePath, spec, k) }) mux.HandleFunc(rootDescPath, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", `text/xml; charset="utf-8"`) w.Header().Set("content-length", fmt.Sprint(len(server.rootDescXML))) w.Header().Set("server", serverField) w.Write(server.rootDescXML) }) handleSCPDs(mux) mux.HandleFunc(serviceControlURL, server.serviceControlHandler) mux.HandleFunc("/debug/pprof/", pprof.Index) // DeviceIcons iconHandl := func(w http.ResponseWriter, r *http.Request) { idStr := path.Base(r.URL.Path) id, _ := strconv.Atoi(idStr) if id < 0 || id >= len(server.Icons) { id = 0 } di := server.Icons[id] w.Header().Set("Content-Type", di.Mimetype) http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(di.Bytes)) } for i := range server.Icons { mux.HandleFunc(fmt.Sprintf("%s/%d", deviceIconPath, i), iconHandl) } } func (s *Server) initServices() (err error) { urn, err := upnp.ParseServiceType(services[0].ServiceType) if err != nil { return } urn1, err := upnp.ParseServiceType(services[1].ServiceType) if err != nil { return } urn2, err := upnp.ParseServiceType(services[2].ServiceType) if err != nil { return } s.services = map[string]UPnPService{ urn.Type: &contentDirectoryService{ Server: s, }, urn1.Type: &connectionManagerService{ Server: s, }, urn2.Type: &mediaReceiverRegistrarService{ Server: s, }, } return } func (srv *Server) Init() (err error) { srv.eventingLogger = srv.Logger.WithNames("eventing") srv.eventingLogger.Levelf(log.Debug, "hello %v", "world") if err = srv.initServices(); err != nil { return } srv.closed = make(chan struct{}) if srv.FriendlyName == "" { srv.FriendlyName = getDefaultFriendlyName() } if srv.HTTPConn == nil { srv.HTTPConn, err = net.Listen("tcp", "") if err != nil { return } } if srv.Interfaces == nil { ifs, err := net.Interfaces() if err != nil { log.Print(err) } var tmp []net.Interface for _, if_ := range ifs { if if_.Flags&net.FlagUp == 0 || if_.MTU <= 0 { continue } tmp = append(tmp, if_) } srv.Interfaces = tmp } if srv.FFProbeCache == nil { srv.FFProbeCache = dummyFFProbeCache{} } srv.httpServeMux = http.NewServeMux() srv.rootDeviceUUID = makeDeviceUuid(srv.FriendlyName) srv.rootDescXML, err = xml.MarshalIndent( upnp.DeviceDesc{ NSDLNA: "urn:schemas-dlna-org:device-1-0", NSSEC: "http://www.sec.co.kr/dlna", SpecVersion: upnp.SpecVersion{Major: 1, Minor: 0}, Device: upnp.Device{ DeviceType: rootDeviceType, FriendlyName: srv.FriendlyName, Manufacturer: "Matt Joiner ", ModelName: rootDeviceModelName, UDN: srv.rootDeviceUUID, VendorXML: ` DMS-1.50 M-DMS-1.50 smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec`, ServiceList: func() (ss []upnp.Service) { for _, s := range services { ss = append(ss, s.Service) } return }(), IconList: func() (ret []upnp.Icon) { for i, di := range srv.Icons { ret = append(ret, upnp.Icon{ Height: di.Height, Width: di.Width, Depth: di.Depth, Mimetype: di.Mimetype, URL: fmt.Sprintf("%s/%d", deviceIconPath, i), }) } return }(), PresentationURL: "/", }, }, " ", " ") if err != nil { return } srv.rootDescXML = append([]byte(``), srv.rootDescXML...) srv.Logger.Println("HTTP srv on", srv.HTTPConn.Addr()) srv.initMux(srv.httpServeMux) srv.ssdpStopped = make(chan struct{}) return nil } // Deprecated: Use Init and then Run. There's a race calling Close on a Server that's had Serve // called on it. func (srv *Server) Serve() (err error) { err = srv.Init() if err != nil { return } return srv.Run() } func (srv *Server) Run() (err error) { go func() { srv.doSSDP() close(srv.ssdpStopped) }() return srv.serveHTTP() } func (srv *Server) Close() (err error) { close(srv.closed) err = srv.HTTPConn.Close() <-srv.ssdpStopped return } func didl_lite(chardata string) string { return `` + chardata + `` } func (me *Server) location(ip net.IP) string { url := url.URL{ Scheme: "http", Host: (&net.TCPAddr{ IP: ip, Port: me.httpPort(), }).String(), Path: rootDescPath, } return url.String() } // Can return nil info with nil err if an earlier Probe gave an error. func (srv *Server) ffmpegProbe(path string) (info *ffprobe.Info, err error) { // We don't want relative paths in the cache. path, err = filepath.Abs(path) if err != nil { return } fi, err := os.Stat(path) if err != nil { return } key := ffmpegInfoCacheKey{path, fi.ModTime().UnixNano()} value, ok := srv.FFProbeCache.Get(key) if !ok { info, err = ffprobe.Run(path) err = suppressFFmpegProbeDataErrors(err) srv.FFProbeCache.Set(key, info) return } info = value.(*ffprobe.Info) return } // IgnorePath detects if a file/directory should be ignored. func (server *Server) IgnorePath(path string) (bool, error) { if !filepath.IsAbs(path) { return false, fmt.Errorf("Path must be absolute: %s", path) } if server.IgnoreHidden { if hidden, err := isHiddenPath(path); err != nil { return false, err } else if hidden { log.Print(path, " ignored: hidden") return true, nil } } if server.IgnoreUnreadable { if readable, err := isReadablePath(path); err != nil { return false, err } else if !readable { log.Print(path, " ignored: unreadable") return true, nil } } return false, nil } func tryToOpenPath(path string) (bool, error) { // Ugly but portable way to check if we can open a file/directory if fh, err := os.Open(path); err == nil { fh.Close() return true, nil } else if !os.IsPermission(err) { return false, err } return false, nil } dms-1.5.0/dlna/dms/dms_others.go000066400000000000000000000003551426116762300165120ustar00rootroot00000000000000//go:build !linux && !darwin && !windows // +build !linux,!darwin,!windows package dms func isHiddenPath(path string) (bool, error) { return false, nil } func isReadablePath(path string) (bool, error) { return tryToOpenPath(path) } dms-1.5.0/dlna/dms/dms_test.go000066400000000000000000000032041426116762300161610ustar00rootroot00000000000000package dms import ( "bytes" "net/http" "runtime" "testing" ) type safeFilePathTestCase struct { root, given, expected string } func TestSafeFilePath(t *testing.T) { var cases []safeFilePathTestCase if runtime.GOOS == "windows" { cases = []safeFilePathTestCase{ {"c:", "/", "c:."}, {"c:", "/test", "c:test"}, {"c:\\", "/", "c:\\"}, {"c:\\", "/test", "c:\\test"}, {"c:\\hello", "../windows", "c:\\hello\\windows"}, {"c:\\hello", "/../windows", "c:\\hello\\windows"}, {"c:\\hello", "/", "c:\\hello"}, {"c:\\hello", "./world", "c:\\hello\\world"}, {"c:\\hello", "/", "c:\\hello"}, // These two ones are invalid but, as this actually prevents to serve them, it is fine {"c:\\foo", "c:/windows/", "c:\\foo\\c:\\windows"}, {"c:\\foo", "e:/", "c:\\foo\\e:"}, } } else { cases = []safeFilePathTestCase{ {"/", "..", "/"}, {"/hello", "..//", "/hello"}, {"", "/precious", "precious"}, {".", "///precious", "precious"}, } } t.Logf("running %d test cases", len(cases)) for _, _case := range cases { a := safeFilePath(_case.root, _case.given) if a != _case.expected { t.Errorf("expected %q from %q and %q but got %q", _case.expected, _case.root, _case.given, a) } } } func TestRequest(t *testing.T) { resp, err := http.NewRequest("NOTIFY", "/", nil) if err != nil { t.Fatal(err) } buf := bytes.NewBuffer(nil) resp.Write(buf) t.Logf("%q", buf.String()) } func TestResponse(t *testing.T) { var resp http.Response resp.StatusCode = http.StatusOK resp.Header = make(http.Header) resp.Header["SID"] = []string{"uuid:1337"} var buf bytes.Buffer resp.Write(&buf) t.Logf("%q", buf.String()) } dms-1.5.0/dlna/dms/dms_unix.go000066400000000000000000000006311426116762300161660ustar00rootroot00000000000000//go:build linux || darwin // +build linux darwin package dms import ( "strings" "golang.org/x/sys/unix" ) func isHiddenPath(path string) (bool, error) { return strings.Contains(path, "/."), nil } func isReadablePath(path string) (bool, error) { err := unix.Access(path, unix.R_OK) switch err { case nil: return true, nil case unix.EACCES: return false, nil default: return false, err } } dms-1.5.0/dlna/dms/dms_unix_test.go000066400000000000000000000011061426116762300172230ustar00rootroot00000000000000//go:build linux || darwin // +build linux darwin package dms import "testing" func TestIsHiddenPath(t *testing.T) { data := map[string]bool{ "/some/path": false, "/some/foo.bar": false, "/some/path/.hidden": true, "/some/.hidden/path": true, "/.hidden/path": true, } for path, expected := range data { if actual, err := isHiddenPath(path); err != nil { t.Errorf("isHiddenPath(%v) returned unexpected error: %s", path, err) } else if expected != actual { t.Errorf("isHiddenPath(%v), expected %v, got %v", path, expected, actual) } } } dms-1.5.0/dlna/dms/dms_windows.go000066400000000000000000000013371426116762300167010ustar00rootroot00000000000000//go:build windows // +build windows package dms import ( "path/filepath" "golang.org/x/sys/windows" ) const hiddenAttributes = windows.FILE_ATTRIBUTE_HIDDEN | windows.FILE_ATTRIBUTE_SYSTEM func isHiddenPath(path string) (hidden bool, err error) { if path == filepath.VolumeName(path)+"\\" { // Volumes always have the "SYSTEM" flag, so do not even test them return false, nil } winPath, err := windows.UTF16PtrFromString(path) if err != nil { return } attrs, err := windows.GetFileAttributes(winPath) if err != nil { return } if attrs&hiddenAttributes != 0 { hidden = true return } return isHiddenPath(filepath.Dir(path)) } func isReadablePath(path string) (bool, error) { return tryToOpenPath(path) } dms-1.5.0/dlna/dms/ffmpeg.go000066400000000000000000000007041426116762300156050ustar00rootroot00000000000000package dms import ( "os/exec" "runtime" "syscall" ) func suppressFFmpegProbeDataErrors(_err error) (err error) { if _err == nil { return } err = _err exitErr, ok := err.(*exec.ExitError) if !ok { return } waitStat, ok := exitErr.Sys().(syscall.WaitStatus) if !ok { return } code := waitStat.ExitStatus() if runtime.GOOS == "windows" { if code == -1094995529 { err = nil } } else if code == 183 { err = nil } return } dms-1.5.0/dlna/dms/html.go000066400000000000000000000006021426116762300153020ustar00rootroot00000000000000package dms import ( "html/template" ) var rootTmpl *template.Template func init() { rootTmpl = template.Must(template.New("root").Parse( `
Path:
`)) } dms-1.5.0/dlna/dms/mimetype.go000066400000000000000000000045471426116762300162030ustar00rootroot00000000000000package dms import ( "mime" "net/http" "os" "path" "strings" "github.com/anacrolix/log" ) func init() { if err := mime.AddExtensionType(".rmvb", "application/vnd.rn-realmedia-vbr"); err != nil { log.Printf("Could not register application/vnd.rn-realmedia-vbr MIME type: %s", err) } if err := mime.AddExtensionType(".ogv", "video/ogg"); err != nil { log.Printf("Could not register video/ogg MIME type: %s", err) } if err := mime.AddExtensionType(".ogg", "audio/ogg"); err != nil { log.Printf("Could not register audio/ogg MIME type: %s", err) } } // Example: "video/mpeg" type mimeType string // IsMedia returns true for media MIME-types func (mt mimeType) IsMedia() bool { return mt.IsVideo() || mt.IsAudio() || mt.IsImage() } // IsVideo returns true for video MIME-types func (mt mimeType) IsVideo() bool { return strings.HasPrefix(string(mt), "video/") || mt == "application/vnd.rn-realmedia-vbr" } // IsAudio returns true for audio MIME-types func (mt mimeType) IsAudio() bool { return strings.HasPrefix(string(mt), "audio/") } // IsImage returns true for image MIME-types func (mt mimeType) IsImage() bool { return strings.HasPrefix(string(mt), "image/") } // Returns the group "type", the part before the '/'. func (mt mimeType) Type() string { return strings.SplitN(string(mt), "/", 2)[0] } // Returns the string representation of this MIME-type func (mt mimeType) String() string { return string(mt) } // MimeTypeByPath determines the MIME-type of file at the given path func MimeTypeByPath(filePath string) (ret mimeType, err error) { ret = mimeTypeByBaseName(path.Base(filePath)) if ret == "" { ret, err = mimeTypeByContent(filePath) } if ret == "video/x-msvideo" { ret = "video/avi" } else if ret == "" { ret = "application/octet-stream" } return } // Guess MIME-type from the extension, ignoring ".part". func mimeTypeByBaseName(name string) mimeType { name = strings.TrimSuffix(name, ".part") ext := path.Ext(name) if ext != "" { return mimeType(mime.TypeByExtension(ext)) } return mimeType("") } // Guess the MIME-type by analysing the first 512 bytes of the file. func mimeTypeByContent(path string) (ret mimeType, err error) { file, err := os.Open(path) if err != nil { return } defer file.Close() var data [512]byte if n, err := file.Read(data[:]); err == nil { ret = mimeType(http.DetectContentType(data[:n])) } return } dms-1.5.0/dlna/dms/mrrs-desc.go000066400000000000000000000047561426116762300162530ustar00rootroot00000000000000package dms const mediaReceiverRegistrarDescription = ` 1 0 IsAuthorized DeviceID in A_ARG_TYPE_DeviceID Result out A_ARG_TYPE_Result RegisterDevice RegistrationReqMsg in A_ARG_TYPE_RegistrationReqMsg RegistrationRespMsg out A_ARG_TYPE_RegistrationRespMsg IsValidated DeviceID in A_ARG_TYPE_DeviceID Result out A_ARG_TYPE_Result A_ARG_TYPE_DeviceID string A_ARG_TYPE_Result int A_ARG_TYPE_RegistrationReqMsg bin.base64 A_ARG_TYPE_RegistrationRespMsg bin.base64 AuthorizationGrantedUpdateID ui4 AuthorizationDeniedUpdateID ui4 ValidationSucceededUpdateID ui4 ValidationRevokedUpdateID ui4 ` dms-1.5.0/dlna/dms/mrrs.go000066400000000000000000000010431426116762300153210ustar00rootroot00000000000000package dms import ( "net/http" "github.com/anacrolix/dms/upnp" ) type mediaReceiverRegistrarService struct { *Server upnp.Eventing } func (mrrs *mediaReceiverRegistrarService) Handle(action string, argsXML []byte, r *http.Request) ([][2]string, error) { switch action { case "IsAuthorized", "IsValidated": return [][2]string{ {"Result", "1"}, }, nil case "RegisterDevice": return [][2]string{ {"RegistrationRespMsg", mrrs.rootDeviceUUID}, }, nil // return nil, nil default: return nil, upnp.InvalidActionError } } dms-1.5.0/go.mod000066400000000000000000000004421426116762300134160ustar00rootroot00000000000000module github.com/anacrolix/dms go 1.16 require ( github.com/anacrolix/ffprobe v1.0.0 github.com/anacrolix/log v0.13.1 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 golang.org/x/net v0.0.0-20220524220425-1d687d428aca golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a ) dms-1.5.0/go.sum000066400000000000000000000144521426116762300134510ustar00rootroot00000000000000github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.0.0 h1:AwZ+mBP4rQ5f7JSsrsN3h7M2xDW/xSE66IPVOqlnuUc= github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/ffprobe v1.0.0 h1:j8fGLBsXejwdXd0pkA9iR3Dt1XwMFv5wjeYWObcue8A= github.com/anacrolix/ffprobe v1.0.0/go.mod h1:BIw+Bjol6CWjm/CRWrVLk2Vy+UYlkgmBZ05vpSYqZPw= github.com/anacrolix/log v0.13.1 h1:BmVwTdxHd5VcNrLylgKwph4P4wf+5VvPgOK4yi91fTY= github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= github.com/anacrolix/missinggo v1.1.0 h1:0lZbaNa6zTR1bELAIzCNmRGAtkHuLDPJqTiTtXoAIx8= github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2 h1:1B/+1BcRhOMG1KH/YhNIU8OppSWk5d/NGyfRla88CuY= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8= golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= dms-1.5.0/helpers/000077500000000000000000000000001426116762300137525ustar00rootroot00000000000000dms-1.5.0/helpers/bsd/000077500000000000000000000000001426116762300145225ustar00rootroot00000000000000dms-1.5.0/helpers/bsd/dms000066400000000000000000000007461426116762300152370ustar00rootroot00000000000000#!/bin/sh . /etc/rc.subr name="dms" rcvar="dms_enable" : ${dms_user:="root"} : ${dms_enable:="NO"} : ${dms_media_dir:="/media"} # Daemon pidfile="/var/run/${name}.pid" command=/usr/sbin/daemon procname="daemon" dms="/usr/local/bin/dms -path ${dms_media_dir}" command_args=" -P ${pidfile} -r -f -u ${dms_user} ${dms}" start_precmd="dms_precmd" pidfile="/var/run/${name}.pid" dms_precmd() { install -o ${dms_user} /dev/null ${pidfile} } load_rc_config $name run_rc_command "$1" dms-1.5.0/helpers/systemd/000077500000000000000000000000001426116762300154425ustar00rootroot00000000000000dms-1.5.0/helpers/systemd/dms.service000066400000000000000000000004501426116762300176060ustar00rootroot00000000000000# Put this file in /home/USERNAME/.config/systemd/user/ # # Enable this service with # systemctl --user --now enable dms.service [Unit] Description=DMS UPnP Media Server [Service] ExecStart=/home/USERNAME/go/bin/dms -friendlyName DMS_Server -path /home/share/ [Install] WantedBy=default.target dms-1.5.0/main.go000066400000000000000000000212321426116762300135630ustar00rootroot00000000000000package main import ( "bytes" _ "embed" "encoding/json" "flag" "fmt" "image" "image/png" "io" "io/ioutil" "net" "os" "os/signal" "os/user" "path/filepath" "runtime" "strings" "sync" "syscall" "time" "github.com/anacrolix/dms/dlna/dms" "github.com/anacrolix/dms/rrcache" "github.com/anacrolix/log" "github.com/nfnt/resize" ) //go:embed "data/VGC Sonic.png" var defaultIcon []byte type dmsConfig struct { Path string IfName string Http string FriendlyName string DeviceIcon string LogHeaders bool FFprobeCachePath string NoTranscode bool ForceTranscodeTo string NoProbe bool StallEventSubscribe bool NotifyInterval time.Duration IgnoreHidden bool IgnoreUnreadable bool AllowedIpNets []*net.IPNet } func (config *dmsConfig) load(configPath string) { file, err := os.Open(configPath) if err != nil { log.Printf("config error (config file: '%s'): %v\n", configPath, err) return } defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&config) if err != nil { log.Printf("config error: %v\n", err) return } } // default config var config = &dmsConfig{ Path: "", IfName: "", Http: ":1338", FriendlyName: "", DeviceIcon: "", LogHeaders: false, FFprobeCachePath: getDefaultFFprobeCachePath(), ForceTranscodeTo: "", } func getDefaultFFprobeCachePath() (path string) { _user, err := user.Current() if err != nil { log.Print(err) return } path = filepath.Join(_user.HomeDir, ".dms-ffprobe-cache") return } type fFprobeCache struct { c *rrcache.RRCache sync.Mutex } func (fc *fFprobeCache) Get(key interface{}) (value interface{}, ok bool) { fc.Lock() defer fc.Unlock() return fc.c.Get(key) } func (fc *fFprobeCache) Set(key interface{}, value interface{}) { fc.Lock() defer fc.Unlock() var size int64 for _, v := range []interface{}{key, value} { b, err := json.Marshal(v) if err != nil { log.Printf("Could not marshal %v: %s", v, err) continue } size += int64(len(b)) } fc.c.Set(key, value, size) } func main() { err := mainErr() if err != nil { log.Fatalf("error in main: %v", err) } } func mainErr() error { path := flag.String("path", config.Path, "browse root path") ifName := flag.String("ifname", config.IfName, "specific SSDP network interface") http := flag.String("http", config.Http, "http server port") friendlyName := flag.String("friendlyName", config.FriendlyName, "server friendly name") deviceIcon := flag.String("deviceIcon", config.DeviceIcon, "device defaultIcon") logHeaders := flag.Bool("logHeaders", config.LogHeaders, "log HTTP headers") fFprobeCachePath := flag.String("fFprobeCachePath", config.FFprobeCachePath, "path to FFprobe cache file") configFilePath := flag.String("config", "", "json configuration file") allowedIps := flag.String("allowedIps", "", "allowed ip of clients, separated by comma") forceTranscodeTo := flag.String("forceTranscodeTo", config.ForceTranscodeTo, "force transcoding to certain format, supported: 'chromecast', 'vp8', 'web'") flag.BoolVar(&config.NoTranscode, "noTranscode", false, "disable transcoding") flag.BoolVar(&config.NoProbe, "noProbe", false, "disable media probing with ffprobe") flag.BoolVar(&config.StallEventSubscribe, "stallEventSubscribe", false, "workaround for some bad event subscribers") flag.DurationVar(&config.NotifyInterval, "notifyInterval", 30*time.Second, "interval between SSPD announces") flag.BoolVar(&config.IgnoreHidden, "ignoreHidden", false, "ignore hidden files and directories") flag.BoolVar(&config.IgnoreUnreadable, "ignoreUnreadable", false, "ignore unreadable files and directories") flag.Parse() if flag.NArg() != 0 { flag.Usage() return fmt.Errorf("%s: %s\n", "unexpected positional arguments", flag.Args()) } logger := log.Default.WithNames("main") config.Path, _ = filepath.Abs(*path) config.IfName = *ifName config.Http = *http config.FriendlyName = *friendlyName config.DeviceIcon = *deviceIcon config.LogHeaders = *logHeaders config.FFprobeCachePath = *fFprobeCachePath config.AllowedIpNets = makeIpNets(*allowedIps) config.ForceTranscodeTo = *forceTranscodeTo logger.Printf("allowed ip nets are %q", config.AllowedIpNets) logger.Printf("serving folder %q", config.Path) if len(*configFilePath) > 0 { config.load(*configFilePath) } cache := &fFprobeCache{ c: rrcache.New(64 << 20), } if err := cache.load(config.FFprobeCachePath); err != nil { log.Print(err) } dmsServer := &dms.Server{ Logger: logger.WithNames("dms", "server"), Interfaces: func(ifName string) (ifs []net.Interface) { var err error if ifName == "" { ifs, err = net.Interfaces() } else { var if_ *net.Interface if_, err = net.InterfaceByName(ifName) if if_ != nil { ifs = append(ifs, *if_) } } if err != nil { log.Fatal(err) } var tmp []net.Interface for _, if_ := range ifs { if if_.Flags&net.FlagUp == 0 || if_.MTU <= 0 { continue } tmp = append(tmp, if_) } ifs = tmp return }(config.IfName), HTTPConn: func() net.Listener { conn, err := net.Listen("tcp", config.Http) if err != nil { log.Fatal(err) } return conn }(), FriendlyName: config.FriendlyName, RootObjectPath: filepath.Clean(config.Path), FFProbeCache: cache, LogHeaders: config.LogHeaders, NoTranscode: config.NoTranscode, ForceTranscodeTo: config.ForceTranscodeTo, NoProbe: config.NoProbe, Icons: []dms.Icon{ { Width: 48, Height: 48, Depth: 8, Mimetype: "image/png", Bytes: readIcon(config.DeviceIcon, 48), }, { Width: 128, Height: 128, Depth: 8, Mimetype: "image/png", Bytes: readIcon(config.DeviceIcon, 128), }, }, StallEventSubscribe: config.StallEventSubscribe, NotifyInterval: config.NotifyInterval, IgnoreHidden: config.IgnoreHidden, IgnoreUnreadable: config.IgnoreUnreadable, AllowedIpNets: config.AllowedIpNets, } if err := dmsServer.Init(); err != nil { log.Fatalf("error initing dms server: %v", err) } go func() { if err := dmsServer.Run(); err != nil { log.Fatal(err) } }() sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) <-sigs err := dmsServer.Close() if err != nil { log.Fatal(err) } if err := cache.save(config.FFprobeCachePath); err != nil { log.Print(err) } return nil } func (cache *fFprobeCache) load(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() dec := json.NewDecoder(f) var items []dms.FfprobeCacheItem err = dec.Decode(&items) if err != nil { return err } for _, item := range items { cache.Set(item.Key, item.Value) } log.Printf("added %d items from cache", len(items)) return nil } func (cache *fFprobeCache) save(path string) error { cache.Lock() items := cache.c.Items() cache.Unlock() f, err := ioutil.TempFile(filepath.Dir(path), filepath.Base(path)) if err != nil { return err } enc := json.NewEncoder(f) err = enc.Encode(items) f.Close() if err != nil { os.Remove(f.Name()) return err } if runtime.GOOS == "windows" { err = os.Remove(path) if err == os.ErrNotExist { err = nil } } if err == nil { err = os.Rename(f.Name(), path) } if err == nil { log.Printf("saved cache with %d items", len(items)) } else { os.Remove(f.Name()) } return err } func getIconReader(path string) (io.ReadCloser, error) { if path == "" { return ioutil.NopCloser(bytes.NewReader(defaultIcon)), nil } return os.Open(path) } func readIcon(path string, size uint) []byte { r, err := getIconReader(path) if err != nil { panic(err) } defer r.Close() imageData, _, err := image.Decode(r) if err != nil { panic(err) } return resizeImage(imageData, size) } func resizeImage(imageData image.Image, size uint) []byte { img := resize.Resize(size, size, imageData, resize.Lanczos3) var buff bytes.Buffer png.Encode(&buff, img) return buff.Bytes() } func makeIpNets(s string) []*net.IPNet { var nets []*net.IPNet if len(s) < 1 { _, ipnet, _ := net.ParseCIDR("0.0.0.0/0") nets = append(nets, ipnet) _, ipnet, _ = net.ParseCIDR("::/0") nets = append(nets, ipnet) } else { for _, el := range strings.Split(s, ",") { ip := net.ParseIP(el) if ip == nil { _, ipnet, err := net.ParseCIDR(el) if err == nil { nets = append(nets, ipnet) } else { log.Printf("unable to parse expression %q", el) } } else { _, ipnet, err := net.ParseCIDR(el + "/32") if err == nil { nets = append(nets, ipnet) } else { log.Printf("unable to parse ip %q", el) } } } } return nets } dms-1.5.0/misc/000077500000000000000000000000001426116762300132435ustar00rootroot00000000000000dms-1.5.0/misc/dms-win32/000077500000000000000000000000001426116762300147665ustar00rootroot00000000000000dms-1.5.0/misc/dms-win32/NOTES000066400000000000000000000005751426116762300156100ustar00rootroot00000000000000This directory should contain a script to build the "dms-win32" package. It's an archive containing the DMS GUI components and all necessary libraries. The script should get the latest static executable builds of ffmpeg, dms, and GTK+ all-in-one into a single folder, with executable files in bin/, and user-facing utilities from this directory in the root, and compress the lot. dms-1.5.0/misc/dms-win32/README.txt000066400000000000000000000003031426116762300164600ustar00rootroot00000000000000Double click the batch file "dms-gui". Select a media directory to share, and enjoy from a DLNA/UPnP enabled device. Try BubbleUPnP from an Android, or VLC's "Local Network>UPnP" interface.dms-1.5.0/misc/dms-win32/dms-gui.bat000066400000000000000000000000511426116762300170170ustar00rootroot00000000000000set PATH=%~dp0\bin start dms-gtk-gui.exedms-1.5.0/misc/misc.go000066400000000000000000000005241426116762300145260ustar00rootroot00000000000000package misc import ( "fmt" "strings" "time" ) func FormatDurationSexagesimal(d time.Duration) string { ns := d % time.Second d /= time.Second s := d % 60 d /= 60 m := d % 60 d /= 60 h := d ret := fmt.Sprintf("%d:%02d:%02d.%09d", h, m, s, ns) ret = strings.TrimRight(ret, "0") ret = strings.TrimRight(ret, ".") return ret } dms-1.5.0/misc/misc_test.go000066400000000000000000000004231426116762300155630ustar00rootroot00000000000000package misc import ( "testing" "time" ) func TestFormatDurationSexagesimal(t *testing.T) { expected := "0:22:57.628452" actual := FormatDurationSexagesimal(time.Duration(1377628452000)) if actual != expected { t.Fatalf("got %q, expected %q", actual, expected) } } dms-1.5.0/play/000077500000000000000000000000001426116762300132555ustar00rootroot00000000000000dms-1.5.0/play/attrs.go000066400000000000000000000005561426116762300147470ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "encoding/xml" "fmt" ) type Meh struct { XMLName xml.Name Size // ChildCount *uint `xml:"childCount,attr"` } func main() { size := uint64(137) data, err := xml.Marshal(Meh{ Size: &xml.Attr{ Name: xml.Name{Local: "size"}, Value: fmt.Sprint(size), }, }) fmt.Println(string(data), err) } dms-1.5.0/play/bool.go000066400000000000000000000001601426116762300145340ustar00rootroot00000000000000//go:build ignore // +build ignore package main import "fmt" func main() { fmt.Printf("%q%c\n", 3, false) } dms-1.5.0/play/browse.xml000066400000000000000000000012361426116762300153020ustar00rootroot00000000000000 0 BrowseDirectChildren dc:title,dc:date,res,res@protocolInfo,res@size,res@duration,res@resolution,res@dlna:ifoFileURI,res@pv:subtitleFileType,res@pv:subtitleFileUri,upnp:albumArtURI,upnp:album,upnp:artist 0 20 dms-1.5.0/play/closure.go000066400000000000000000000005111426116762300152550ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "fmt" ) func main() { ch := make(chan struct{}) for i := 0; i < 5; i++ { j := i go func() { fmt.Print(j) ch <- struct{}{} }() } for i := 5; i < 10; i++ { go func() { fmt.Print(i) ch <- struct{}{} }() } for i := 0; i < 10; i++ { <-ch } } dms-1.5.0/play/execbug.go000066400000000000000000000005601426116762300152270ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "io" "os" "os/exec" "time" ) func main() { cmd := exec.Command("ls") out, err := cmd.StdoutPipe() if err != nil { panic(err) } if err = cmd.Start(); err != nil { panic(err) } go cmd.Wait() time.Sleep(10 * time.Millisecond) _, err = io.Copy(os.Stdout, out) if err != nil { panic(err) } } dms-1.5.0/play/execgood.go000066400000000000000000000007511426116762300154040ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "io" "os" "os/exec" "time" "github.com/anacrolix/log" ) func main() { cmd := exec.Command("ls") out, err := cmd.StdoutPipe() if err != nil { panic(err) } if err := cmd.Start(); err != nil { panic(err) } r, w := io.Pipe() go func() { io.Copy(w, out) out.Close() w.Close() log.Println(cmd.Wait()) }() time.Sleep(10 * time.Millisecond) if _, err := io.Copy(os.Stdout, r); err != nil { panic(err) } } dms-1.5.0/play/ffprobe.go000066400000000000000000000004451426116762300152320ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "flag" "github.com/anacrolix/ffprobe" "github.com/anacrolix/log" ) func main() { log.SetFlags(log.Llongfile) flag.Parse() for _, path := range flag.Args() { i, err := ffprobe.Probe(path) log.Printf("%#v %#v", i, err) } } dms-1.5.0/play/getsortcaps.xml000066400000000000000000000004641426116762300163410ustar00rootroot00000000000000 dms-1.5.0/play/mime.go000066400000000000000000000003361426116762300145350ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "flag" "fmt" "github.com/anacrolix/dms/dlna/dms" ) func main() { flag.Parse() for _, arg := range flag.Args() { fmt.Println(dms.MimeTypeByPath(arg)) } } dms-1.5.0/play/parse_http_version.go000066400000000000000000000002601426116762300175200ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "fmt" "net/http" "strings" ) func main() { fmt.Println(http.ParseHTTPVersion(strings.TrimSpace("HTTP/1.1 "))) } dms-1.5.0/play/print-ifs.go000066400000000000000000000007741426116762300155270ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "fmt" "net" ) func main() { ifs, err := net.Interfaces() if err != nil { panic(err) } for _, if_ := range ifs { fmt.Printf("%#v\n", if_) addrs, err := if_.Addrs() if err != nil { panic(err) } for _, addr := range addrs { fmt.Printf("\t%s %s\n", addr.Network(), addr) } mcastAddrs, err := if_.MulticastAddrs() if err != nil { panic(err) } for _, addr := range mcastAddrs { fmt.Printf("\t%s\n", addr) } } } dms-1.5.0/play/scpd.go000066400000000000000000000014031426116762300145330ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "encoding/xml" "fmt" "github.com/anacrolix/dms/upnp" "github.com/anacrolix/log" ) func main() { scpd := upnp.SCPD{ SpecVersion: upnp.SpecVersion{Major: 1, Minor: 0}, ActionList: []upnp.Action{ { Name: "Browse", Arguments: []upnp.Argument{ {Name: "ObjectID", Direction: "in", RelatedStateVar: "A_ARG_TYPE_ObjectID"}, }, }, }, ServiceStateTable: []upnp.StateVariable{ { SendEvents: "no", Name: "A_ARG_TYPE_ObjectID", DataType: "string", AllowedValues: &[]string{"hi", "there"}, }, { SendEvents: "yes", Name: "loltype", }, }, } xml, err := xml.MarshalIndent(scpd, "", " ") if err != nil { log.Fatalln(err) } fmt.Print(string(xml)) } dms-1.5.0/play/soap.go000066400000000000000000000016311426116762300145470ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "encoding/xml" "fmt" "io/ioutil" "os" "github.com/anacrolix/dms/soap" ) type Browse struct { ObjectID string BrowseFlag string Filter string StartingIndex int RequestedCount int } type GetSortCapabilitiesResponse struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:service:ContentDirectory:1 GetSortCapabilitiesResponse"` SortCaps string } func main() { raw, err := ioutil.ReadAll(os.Stdin) if err != nil { panic(err) } var env soap.Envelope if err := xml.Unmarshal(raw, &env); err != nil { panic(err) } fmt.Println(env) var browse Browse err = xml.Unmarshal([]byte(env.Body.Action), &browse) if err != nil { panic(err) } fmt.Println(browse) raw, err = xml.MarshalIndent( GetSortCapabilitiesResponse{ SortCaps: "dc:title", }, "", " ") if err != nil { panic(err) } fmt.Println(string(raw)) } dms-1.5.0/play/termsig/000077500000000000000000000000001426116762300147275ustar00rootroot00000000000000dms-1.5.0/play/termsig/main.go000066400000000000000000000002401426116762300161760ustar00rootroot00000000000000package main import ( "fmt" "os" "os/signal" ) func main() { c := make(chan os.Signal, 0x100) signal.Notify(c) for i := range c { fmt.Println(i) } } dms-1.5.0/play/transcode.go000066400000000000000000000012011426116762300155600ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "bufio" "flag" "io" "os" "time" "github.com/anacrolix/log" "github.com/anacrolix/dms/misc" ) func main() { ss := flag.String("ss", "", "") t := flag.String("t", "", "") flag.Parse() if flag.NArg() != 1 { log.Fatalln("wrong argument count") } r, err := misc.Transcode(flag.Arg(0), *ss, *t) if err != nil { log.Fatalln(err) } go func() { buf := bufio.NewWriterSize(os.Stdout, 1234) n, err := io.Copy(buf, r) log.Println("copied", n, "bytes") if err != nil { log.Println(err) } }() time.Sleep(time.Second) go r.Close() time.Sleep(time.Second) } dms-1.5.0/play/url.go000066400000000000000000000003611426116762300144060ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "fmt" "net/url" "github.com/anacrolix/log" ) func main() { url_, err := url.Parse("[192:168:26:2::3]:1900") if err != nil { log.Fatalln(err) } fmt.Printf("%#v\n", url_) } dms-1.5.0/rrcache/000077500000000000000000000000001426116762300137175ustar00rootroot00000000000000dms-1.5.0/rrcache/rrcache.go000066400000000000000000000030531426116762300156560ustar00rootroot00000000000000// Package rrcache implements a random replacement cache. Items are set with // an associated size. When the capacity is exceeded, items will be randomly // evicted until it is not. package rrcache import ( "math/rand" ) type RRCache struct { capacity int64 size int64 keys []interface{} table map[interface{}]*entry } type entry struct { size int64 value interface{} } func New(capacity int64) *RRCache { return &RRCache{ capacity: capacity, table: make(map[interface{}]*entry), } } // Returns the sum size of all items currently in the cache. func (c *RRCache) Size() int64 { return c.size } func (c *RRCache) Set(key interface{}, value interface{}, size int64) { if size > c.capacity { return } _entry := c.table[key] if _entry == nil { _entry = new(entry) c.keys = append(c.keys, key) c.table[key] = _entry } sizeDelta := size - _entry.size _entry.value = value _entry.size = size c.size += sizeDelta for c.size > c.capacity { i := rand.Intn(len(c.keys)) key := c.keys[i] c.keys[i] = c.keys[len(c.keys)-1] c.keys = c.keys[:len(c.keys)-1] c.size -= c.table[key].size delete(c.table, key) } } func (c *RRCache) Get(key interface{}) (value interface{}, ok bool) { entry, ok := c.table[key] if !ok { return } value = entry.value return } type Item struct { Key, Value interface{} } // Return all items currently in the cache. This is made available for // serialization purposes. func (c *RRCache) Items() (itens []Item) { for k, e := range c.table { itens = append(itens, Item{k, e.value}) } return } dms-1.5.0/soap/000077500000000000000000000000001426116762300132525ustar00rootroot00000000000000dms-1.5.0/soap/soap.go000066400000000000000000000026521426116762300145500ustar00rootroot00000000000000package soap import ( "encoding/xml" ) const ( EncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" EnvelopeNS = "http://schemas.xmlsoap.org/soap/envelope/" ) type Arg struct { XMLName xml.Name Value string `xml:",chardata"` } type Action struct { XMLName xml.Name Args []Arg } type Body struct { Action []byte `xml:",innerxml"` } type UPnPError struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:control-1-0 UPnPError"` Code uint `xml:"errorCode"` Desc string `xml:"errorDescription"` } type FaultDetail struct { XMLName xml.Name `xml:"detail"` Data interface{} } type Fault struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` FaultCode string `xml:"faultcode"` FaultString string `xml:"faultstring"` Detail FaultDetail `xml:"detail"` } func NewFault(s string, detail interface{}) *Fault { return &Fault{ FaultCode: EnvelopeNS + ":Client", FaultString: s, Detail: FaultDetail{ Data: detail, }, } } type Envelope struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` EncodingStyle string `xml:"encodingStyle,attr"` Body Body `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` } /* XML marshalling of nested namespaces is broken. func NewEnvelope(action []byte) Envelope { return Envelope{ EncodingStyle: EncodingStyle, Body: Body{action}, } } */ dms-1.5.0/ssdp/000077500000000000000000000000001426116762300132615ustar00rootroot00000000000000dms-1.5.0/ssdp/ssdp.go000066400000000000000000000167141426116762300145720ustar00rootroot00000000000000package ssdp import ( "bufio" "bytes" "fmt" "io" "math/rand" "net" "net/http" "net/textproto" "strconv" "strings" "time" "github.com/anacrolix/log" "golang.org/x/net/ipv4" ) const ( AddrString = "239.255.255.250:1900" rootDevice = "upnp:rootdevice" aliveNTS = "ssdp:alive" byebyeNTS = "ssdp:byebye" ) var NetAddr *net.UDPAddr func init() { var err error NetAddr, err = net.ResolveUDPAddr("udp4", AddrString) if err != nil { log.Printf("Could not resolve %s: %s", AddrString, err) } } type badStringError struct { what string str string } func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } func ReadRequest(b *bufio.Reader) (req *http.Request, err error) { tp := textproto.NewReader(b) var s string if s, err = tp.ReadLine(); err != nil { return nil, err } defer func() { if err == io.EOF { err = io.ErrUnexpectedEOF } }() var f []string // TODO a split that only allows N values? if f = strings.SplitN(s, " ", 3); len(f) < 3 { return nil, &badStringError{"malformed request line", s} } if f[1] != "*" { return nil, &badStringError{"bad URL request", f[1]} } req = &http.Request{ Method: f[0], } var ok bool if req.ProtoMajor, req.ProtoMinor, ok = http.ParseHTTPVersion(strings.TrimSpace(f[2])); !ok { return nil, &badStringError{"malformed HTTP version", f[2]} } mimeHeader, err := tp.ReadMIMEHeader() if err != nil { return nil, err } req.Header = http.Header(mimeHeader) return } type Server struct { conn *net.UDPConn Interface net.Interface Server string Services []string Devices []string IPFilter func(net.IP) bool Location func(net.IP) string UUID string NotifyInterval time.Duration closed chan struct{} Logger log.Logger } func makeConn(ifi net.Interface) (ret *net.UDPConn, err error) { ret, err = net.ListenMulticastUDP("udp", &ifi, NetAddr) if err != nil { return } p := ipv4.NewPacketConn(ret) if err := p.SetMulticastTTL(2); err != nil { log.Print(err) } // if err := p.SetMulticastLoopback(true); err != nil { // log.Println(err) // } return } func (me *Server) serve() { for { size := me.Interface.MTU if size > 65536 { size = 65536 } else if size <= 0 { // fix for windows with mtu 4gb size = 65536 } b := make([]byte, size) n, addr, err := me.conn.ReadFromUDP(b) select { case <-me.closed: return default: } if err != nil { me.Logger.Printf("error reading from UDP socket: %s", err) break } go me.handle(b[:n], addr) } } func (me *Server) Init() (err error) { me.closed = make(chan struct{}) me.conn, err = makeConn(me.Interface) if me.IPFilter == nil { me.IPFilter = func(net.IP) bool { return true } } return } func (me *Server) Close() { close(me.closed) me.sendByeBye() me.conn.Close() } func (me *Server) Serve() (err error) { go me.serve() for { addrs, err := me.Interface.Addrs() if err != nil { return err } for _, addr := range addrs { ip := func() net.IP { switch val := addr.(type) { case *net.IPNet: return val.IP case *net.IPAddr: return val.IP } panic(fmt.Sprint("unexpected addr type:", addr)) }() if !me.IPFilter(ip) { continue } if ip.IsLinkLocalUnicast() { // These addresses seem to confuse VLC. Possibly there's supposed to be a zone // included in the address, but I don't see one. continue } extraHdrs := [][2]string{ {"CACHE-CONTROL", fmt.Sprintf("max-age=%d", 5*me.NotifyInterval/2/time.Second)}, {"LOCATION", me.Location(ip)}, } me.notifyAll(aliveNTS, extraHdrs) } time.Sleep(me.NotifyInterval) } } func (me *Server) usnFromTarget(target string) string { if target == me.UUID { return target } return me.UUID + "::" + target } func (me *Server) makeNotifyMessage(target, nts string, extraHdrs [][2]string) []byte { lines := [...][2]string{ {"HOST", AddrString}, {"NT", target}, {"NTS", nts}, {"SERVER", me.Server}, {"USN", me.usnFromTarget(target)}, } buf := &bytes.Buffer{} fmt.Fprint(buf, "NOTIFY * HTTP/1.1\r\n") writeHdr := func(keyValue [2]string) { fmt.Fprintf(buf, "%s: %s\r\n", keyValue[0], keyValue[1]) } for _, pair := range lines { writeHdr(pair) } for _, pair := range extraHdrs { writeHdr(pair) } fmt.Fprint(buf, "\r\n") return buf.Bytes() } func (me *Server) send(buf []byte, addr *net.UDPAddr) { if n, err := me.conn.WriteToUDP(buf, addr); err != nil { me.Logger.Printf("error writing to UDP socket: %s", err) } else if n != len(buf) { me.Logger.Printf("short write: %d/%d bytes", n, len(buf)) } } func (me *Server) delayedSend(delay time.Duration, buf []byte, addr *net.UDPAddr) { go func() { select { case <-time.After(delay): me.send(buf, addr) case <-me.closed: } }() } func (me *Server) log(args ...interface{}) { args = append([]interface{}{me.Interface.Name + ":"}, args...) me.Logger.Print(args...) } func (me *Server) sendByeBye() { for _, type_ := range me.allTypes() { buf := me.makeNotifyMessage(type_, byebyeNTS, nil) me.send(buf, NetAddr) } } func (me *Server) notifyAll(nts string, extraHdrs [][2]string) { for _, type_ := range me.allTypes() { buf := me.makeNotifyMessage(type_, nts, extraHdrs) delay := time.Duration(rand.Int63n(int64(100 * time.Millisecond))) me.delayedSend(delay, buf, NetAddr) } } func (me *Server) allTypes() (ret []string) { for _, a := range [][]string{ {rootDevice, me.UUID}, me.Devices, me.Services, } { ret = append(ret, a...) } return } func (me *Server) handle(buf []byte, sender *net.UDPAddr) { req, err := ReadRequest(bufio.NewReader(bytes.NewReader(buf))) if err != nil { me.Logger.Println(err) return } if req.Method != "M-SEARCH" || req.Header.Get("man") != `"ssdp:discover"` { return } var mx uint if req.Header.Get("Host") == AddrString { mxHeader := req.Header.Get("mx") i, err := strconv.ParseUint(mxHeader, 0, 0) if err != nil { me.Logger.Printf("Invalid mx header %q: %s", mxHeader, err) return } mx = uint(i) } else { mx = 1 } types := func(st string) []string { if st == "ssdp:all" { return me.allTypes() } for _, t := range me.allTypes() { if t == st { return []string{t} } } return nil }(req.Header.Get("st")) for _, ip := range func() (ret []net.IP) { addrs, err := me.Interface.Addrs() if err != nil { panic(err) } for _, addr := range addrs { if ip, ok := func() (net.IP, bool) { switch data := addr.(type) { case *net.IPNet: if data.Contains(sender.IP) { return data.IP, true } return nil, false case *net.IPAddr: return data.IP, true } panic(addr) }(); ok { ret = append(ret, ip) } } return }() { for _, type_ := range types { resp := me.makeResponse(ip, type_, req) delay := time.Duration(rand.Int63n(int64(time.Second) * int64(mx))) me.delayedSend(delay, resp, sender) } } } func (me *Server) makeResponse(ip net.IP, targ string, req *http.Request) (ret []byte) { resp := &http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header), Request: req, } for _, pair := range [...][2]string{ {"CACHE-CONTROL", fmt.Sprintf("max-age=%d", 5*me.NotifyInterval/2/time.Second)}, {"EXT", ""}, {"LOCATION", me.Location(ip)}, {"SERVER", me.Server}, {"ST", targ}, {"USN", me.usnFromTarget(targ)}, } { resp.Header.Set(pair[0], pair[1]) } buf := &bytes.Buffer{} if err := resp.Write(buf); err != nil { panic(err) } return buf.Bytes() } dms-1.5.0/transcode/000077500000000000000000000000001426116762300142725ustar00rootroot00000000000000dms-1.5.0/transcode/transcode.go000066400000000000000000000101421426116762300166010ustar00rootroot00000000000000// Package transcode implements routines for transcoding to various kinds of // receiver. package transcode import ( "io" "os/exec" "runtime" "strconv" "time" "github.com/anacrolix/log" . "github.com/anacrolix/dms/misc" "github.com/anacrolix/ffprobe" ) // Invokes an external command and returns a reader from its stdout. The // command is waited on asynchronously. func transcodePipe(args []string, stderr io.Writer) (r io.ReadCloser, err error) { log.Println("transcode command:", args) cmd := exec.Command(args[0], args[1:]...) cmd.Stderr = stderr r, err = cmd.StdoutPipe() if err != nil { return } err = cmd.Start() if err != nil { return } go func() { err := cmd.Wait() if err != nil { log.Printf("command %s failed: %s", args, err) } }() return } // Return a series of ffmpeg arguments that pick specific codecs for specific // streams. This requires use of the -map flag. func streamArgs(s map[string]interface{}) (ret []string) { defer func() { if len(ret) != 0 { ret = append(ret, []string{ "-map", "0:" + strconv.Itoa(int(s["index"].(float64))), }...) } }() switch s["codec_type"] { case "video": /* if s["codec_name"] == "h264" { if i, _ := strconv.ParseInt(s["is_avc"], 0, 0); i != 0 { return []string{"-vcodec", "copy", "-sameq", "-vbsf", "h264_mp4toannexb"} } } */ return []string{"-target", "pal-dvd"} case "audio": if s["codec_name"] == "dca" { return []string{"-acodec", "ac3", "-ab", "224k", "-ac", "2"} } else { return []string{"-acodec", "copy"} } case "subtitle": return []string{"-scodec", "copy"} } return } // Streams the desired file in the MPEG_PS_PAL DLNA profile. func Transcode(path string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) { args := []string{ "ffmpeg", "-threads", strconv.FormatInt(int64(runtime.NumCPU()), 10), "-async", "1", "-ss", FormatDurationSexagesimal(start), } if length >= 0 { args = append(args, []string{ "-t", FormatDurationSexagesimal(length), }...) } args = append(args, []string{ "-i", path, }...) info, err := ffprobe.Run(path) if err != nil { return } for _, s := range info.Streams { args = append(args, streamArgs(s)...) } args = append(args, []string{"-f", "mpegts", "pipe:"}...) return transcodePipe(args, stderr) } // Returns a stream of Chromecast supported VP8. func VP8Transcode(path string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) { args := []string{ "avconv", "-threads", strconv.FormatInt(int64(runtime.NumCPU()), 10), "-async", "1", "-ss", FormatDurationSexagesimal(start), } if length > 0 { args = append(args, []string{ "-t", FormatDurationSexagesimal(length), }...) } args = append(args, []string{ "-i", path, // "-deadline", "good", // "-c:v", "libvpx", "-crf", "10", "-f", "webm", "pipe:", }...) return transcodePipe(args, stderr) } // Returns a stream of Chromecast supported matroska. func ChromecastTranscode(path string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) { args := []string{ "ffmpeg", "-ss", FormatDurationSexagesimal(start), "-i", path, "-c:v", "libx264", "-preset", "ultrafast", "-profile:v", "high", "-level", "5.0", "-movflags", "+faststart+frag_keyframe+empty_moov", } if length > 0 { args = append(args, []string{ "-t", FormatDurationSexagesimal(length), }...) } args = append(args, []string{ "-f", "mp4", "pipe:", }...) return transcodePipe(args, stderr) } // Returns a stream of h264 video and mp3 audio func WebTranscode(path string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) { args := []string{ "ffmpeg", "-ss", FormatDurationSexagesimal(start), "-i", path, "-pix_fmt", "yuv420p", "-c:v", "libx264", "-crf", "25", "-c:a", "mp3", "-ab", "128k", "-ar", "44100", "-preset", "ultrafast", "-movflags", "+faststart+frag_keyframe+empty_moov", } if length > 0 { args = append(args, []string{ "-t", FormatDurationSexagesimal(length), }...) } args = append(args, []string{ "-f", "mp4", "pipe:", }...) return transcodePipe(args, stderr) } dms-1.5.0/upnp/000077500000000000000000000000001426116762300132725ustar00rootroot00000000000000dms-1.5.0/upnp/eventing.go000066400000000000000000000044771426116762300154540ustar00rootroot00000000000000package upnp import ( "crypto/rand" "encoding/xml" "fmt" "io" "net/url" "regexp" "time" "github.com/anacrolix/log" ) // TODO: Why use namespace prefixes in PropertySet et al? Because the spec // uses them, and I believe the Golang standard library XML spec implementers // incorrectly assume that you can get away with just xmlns="". // propertyset is the root element sent in an event callback. type PropertySet struct { XMLName struct{} `xml:"e:propertyset"` Properties []Property // This should be set to `"urn:schemas-upnp-org:event-1-0"`. Space string `xml:"xmlns:e,attr"` } // propertys provide namespacing to the contained variables. type Property struct { XMLName struct{} `xml:"e:property"` Variable Variable } // Represents an evented state variable that has sendEvents="yes" in its // service spec. type Variable struct { XMLName xml.Name Value string `xml:",chardata"` } type subscriber struct { sid string nextSeq uint32 // 0 for initial event, wraps from Uint32Max to 1. urls []*url.URL expiry time.Time } // Intended to eventually be an embeddable implementation for managing // eventing for a service. Not complete. type Eventing struct { subscribers map[string]*subscriber } func (me *Eventing) Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error) { var uuid [16]byte io.ReadFull(rand.Reader, uuid[:]) sid = FormatUUID(uuid[:]) if _, ok := me.subscribers[sid]; ok { err = fmt.Errorf("already subscribed: %s", sid) return } ssr := &subscriber{ sid: sid, urls: callback, expiry: time.Now().Add(time.Duration(timeoutSeconds) * time.Second), } if me.subscribers == nil { me.subscribers = make(map[string]*subscriber) } me.subscribers[sid] = ssr actualTimeout = int(ssr.expiry.Sub(time.Now()) / time.Second) return } func (me *Eventing) Unsubscribe(sid string) error { return nil } var callbackURLRegexp = regexp.MustCompile("<(.*?)>") // Parse the CALLBACK HTTP header in an event subscription request. See UPnP // Device Architecture 4.1.2. func ParseCallbackURLs(callback string) (ret []*url.URL) { for _, match := range callbackURLRegexp.FindAllStringSubmatch(callback, -1) { _url, err := url.Parse(match[1]) if err != nil { log.Printf("bad callback url: %q", match[1]) continue } ret = append(ret, _url) } return } dms-1.5.0/upnp/eventing_test.go000066400000000000000000000014041426116762300164760ustar00rootroot00000000000000package upnp import ( "encoding/xml" "testing" ) // Visually verify that property sets are marshalled correctly. func TestMarshalPropertySet(t *testing.T) { b, err := xml.MarshalIndent(&PropertySet{ Properties: []Property{ { Variable: Variable{ XMLName: xml.Name{ Local: "SystemUpdateID", }, Value: "0", }, }, { Variable: Variable{ XMLName: xml.Name{ Local: "answerToTheUniverse", }, Value: "42", }, }, }, Space: "urn:schemas-upnp-org:event-1-0", }, "", " ") t.Log("\n" + string(b)) if err != nil { t.Fatal(err) } } func TestParseCallbackURLs(t *testing.T) { urls := ParseCallbackURLs(" ") if len(urls) != 3 { t.Fatal(len(urls)) } } dms-1.5.0/upnp/upnp.go000066400000000000000000000100311426116762300145760ustar00rootroot00000000000000package upnp import ( "encoding/xml" "errors" "fmt" "regexp" "strconv" "strings" "github.com/anacrolix/log" ) var serviceURNRegexp *regexp.Regexp = regexp.MustCompile(`^urn:(.*):service:(\w+):(\d+)$`) type ServiceURN struct { Auth string Type string Version uint64 } func (me ServiceURN) String() string { return fmt.Sprintf("urn:%s:service:%s:%d", me.Auth, me.Type, me.Version) } func ParseServiceType(s string) (ret ServiceURN, err error) { matches := serviceURNRegexp.FindStringSubmatch(s) if matches == nil { err = errors.New(s) return } if len(matches) != 4 { log.Panicf("Invalid serviceURNRegexp?") } ret.Auth = matches[1] ret.Type = matches[2] ret.Version, err = strconv.ParseUint(matches[3], 0, 0) return } type SoapAction struct { ServiceURN Action string } func ParseActionHTTPHeader(s string) (ret SoapAction, err error) { if len(s) < 3 { return } if s[0] != '"' || s[len(s)-1] != '"' { return } s = s[1 : len(s)-1] hashIndex := strings.LastIndex(s, "#") if hashIndex == -1 { return } ret.Action = s[hashIndex+1:] ret.ServiceURN, err = ParseServiceType(s[:hashIndex]) return } type SpecVersion struct { Major int `xml:"major"` Minor int `xml:"minor"` } type Icon struct { Mimetype string `xml:"mimetype"` Width int `xml:"width"` Height int `xml:"height"` Depth int `xml:"depth"` URL string `xml:"url"` } type Service struct { XMLName xml.Name `xml:"service"` ServiceType string `xml:"serviceType"` ServiceId string `xml:"serviceId"` SCPDURL string ControlURL string `xml:"controlURL"` EventSubURL string `xml:"eventSubURL"` } type Device struct { DeviceType string `xml:"deviceType"` FriendlyName string `xml:"friendlyName"` Manufacturer string `xml:"manufacturer"` ModelName string `xml:"modelName"` UDN string VendorXML string `xml:",innerxml"` IconList []Icon `xml:"iconList>icon"` ServiceList []Service `xml:"serviceList>service"` PresentationURL string `xml:"presentationURL,omitempty"` } type DeviceDesc struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:device-1-0 root"` NSDLNA string `xml:"xmlns:dlna,attr"` NSSEC string `xml:"xmlns:sec,attr"` SpecVersion SpecVersion `xml:"specVersion"` Device Device `xml:"device"` } type Error struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:control-1-0 UPnPError"` Code uint `xml:"errorCode"` Desc string `xml:"errorDescription"` } func (e *Error) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Desc) } const ( InvalidActionErrorCode = 401 ActionFailedErrorCode = 501 ArgumentValueInvalidErrorCode = 600 ) var ( InvalidActionError = Errorf(401, "Invalid Action") ArgumentValueInvalidError = Errorf(600, "The argument value is invalid") ) // Errorf creates an UPNP error from the given code and description func Errorf(code uint, tpl string, args ...interface{}) *Error { return &Error{Code: code, Desc: fmt.Sprintf(tpl, args...)} } // ConvertError converts any error to an UPNP error func ConvertError(err error) *Error { if err == nil { return nil } if e, ok := err.(*Error); ok { return e } return Errorf(ActionFailedErrorCode, err.Error()) } type Action struct { Name string Arguments []Argument } type Argument struct { Name string Direction string RelatedStateVar string } type SCPD struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:service-1-0 scpd"` SpecVersion SpecVersion `xml:"specVersion"` ActionList []Action `xml:"actionList>action"` ServiceStateTable []StateVariable `xml:"serviceStateTable>stateVariable"` } type StateVariable struct { SendEvents string `xml:"sendEvents,attr"` Name string `xml:"name"` DataType string `xml:"dataType"` AllowedValues *[]string `xml:"allowedValueList>allowedValue,omitempty"` } func FormatUUID(buf []byte) string { return fmt.Sprintf("uuid:%x-%x-%x-%x-%x", buf[:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16]) } dms-1.5.0/upnpav/000077500000000000000000000000001426116762300136215ustar00rootroot00000000000000dms-1.5.0/upnpav/upnpav.go000066400000000000000000000034201426116762300154600ustar00rootroot00000000000000package upnpav import ( "encoding/xml" "time" ) const ( // NoSuchObjectErrorCode : The specified ObjectID is invalid. NoSuchObjectErrorCode = 701 ) // Resource description type Resource struct { XMLName xml.Name `xml:"res"` ProtocolInfo string `xml:"protocolInfo,attr"` URL string `xml:",chardata"` Size uint64 `xml:"size,attr,omitempty"` Bitrate uint `xml:"bitrate,attr,omitempty"` Duration string `xml:"duration,attr,omitempty"` Resolution string `xml:"resolution,attr,omitempty"` } // Container description type Container struct { Object XMLName xml.Name `xml:"container"` ChildCount int `xml:"childCount,attr"` } // Item description type Item struct { Object XMLName xml.Name `xml:"item"` Res []Resource InnerXML string `xml:",innerxml"` } // Object description type Object struct { ID string `xml:"id,attr"` ParentID string `xml:"parentID,attr"` Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable Title string `xml:"dc:title"` Class string `xml:"upnp:class"` Icon string `xml:"upnp:icon,omitempty"` Date Timestamp `xml:"dc:date"` Artist string `xml:"upnp:artist,omitempty"` Album string `xml:"upnp:album,omitempty"` Genre string `xml:"upnp:genre,omitempty"` AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` Searchable int `xml:"searchable,attr"` SearchXML string `xml:",innerxml"` } // Timestamp wraps time.Time for formatting purposes type Timestamp struct { time.Time } // MarshalXML formats the Timestamp per DIDL-Lite spec func (t Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(t.Format("2006-01-02"), start) } dms-1.5.0/version/000077500000000000000000000000001426116762300137755ustar00rootroot00000000000000dms-1.5.0/version/version.go000066400000000000000000000000521426116762300160060ustar00rootroot00000000000000package version const DmsVersion = "1.4"