pax_global_header00006660000000000000000000000064144265710470014524gustar00rootroot0000000000000052 comment=669cd176b53a2300794168a021d91341dc2c02c3 anacrolix-dms-669cd17/000077500000000000000000000000001442657104700146325ustar00rootroot00000000000000anacrolix-dms-669cd17/.circleci/000077500000000000000000000000001442657104700164655ustar00rootroot00000000000000anacrolix-dms-669cd17/.circleci/config.yml000066400000000000000000000033121442657104700204540ustar00rootroot00000000000000version: 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 anacrolix-dms-669cd17/.github/000077500000000000000000000000001442657104700161725ustar00rootroot00000000000000anacrolix-dms-669cd17/.github/dependabot.yml000066400000000000000000000002421442657104700210200ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" anacrolix-dms-669cd17/.github/goreleaser.yml000066400000000000000000000021521442657104700210450ustar00rootroot00000000000000# .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 anacrolix-dms-669cd17/.github/workflows/000077500000000000000000000000001442657104700202275ustar00rootroot00000000000000anacrolix-dms-669cd17/.github/workflows/docker-publish.yml000066400000000000000000000016641442657104700236740ustar00rootroot00000000000000name: Create and publish Container Images on: push jobs: build-container-images: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup QEMU uses: docker/setup-qemu-action@v2 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v2 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: | ghcr.io/${{ github.repository }}:latest labels: ${{ steps.meta.outputs.labels }} anacrolix-dms-669cd17/.github/workflows/go.yml000066400000000000000000000050261442657104700213620ustar00rootroot00000000000000name: 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@v4 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 anacrolix-dms-669cd17/Dockerfile000066400000000000000000000006241442657104700166260ustar00rootroot00000000000000FROM docker.io/alpine:edge AS build WORKDIR /dms ADD . /dms RUN apk add --no-cache go gcc musl-dev RUN go mod tidy RUN go build -trimpath -buildmode=pie -ldflags="-s -w" -o dms FROM docker.io/alpine:edge COPY --from=build --chown=1000:1000 /dms/dms /dms RUN apk add --no-cache ffmpeg ffmpegthumbnailer mailcap RUN adduser user || true USER user:user WORKDIR /dmsdir VOLUME /dmsdir ENTRYPOINT ["/dms"] anacrolix-dms-669cd17/LICENSE000066400000000000000000000030071442657104700156370ustar00rootroot00000000000000Copyright (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.anacrolix-dms-669cd17/README.rst000077500000000000000000000110441442657104700163240ustar00rootroot00000000000000dms === 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 also supports serving dynamic streams (e.g. a live rtsp stream) generated on the fly with the help of an external application (e.g. ffmpeg). 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 using Docker ======================== `dms` is distributed as Docker Image. Serve Media in `/mediadirectory` using `dms`: .. code-block:: bash docker pull ghcr.io/anacrolix/dms:latest docker run -d --network host -v /mediadirectory:/dmsdir ghcr.io/anacrolix/dms:latest 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. * Roku devices Usage of dms: ===================== .. list-table:: Usage :widths: auto :header-rows: 1 * - parameter - description * - ``-allowDynamicStreams`` - turns on support for `.dms.json` files in the path * - ``-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 * - ``-transcodeLogPattern`` - pattern where to write transcode logs to. The ``[tsname]`` placeholder is replaced with the name of the item currently being played. The default is ``$HOME/.dms/log/[tsname]``. You may turn off transcode logging entirely by setting it to ``/dev/null``. You may log to stderr by setting ``/dev/stderr``. Dynamic streams =============== DMS supports "dynamic streams" generated on the fly. This feature can be activated with the ``-allowDynamicStreams`` command line flag and can be configured by placing special metadata files in your content directory. The name of these metadata files ends with ``.dms.json``, their structure is `documented here `_. An example:: { "Title": "My awesome webcam", "Resources": [ { "MimeType": "video/webm", "Command": "ffmpeg -i rtsp://10.6.8.161:554/Streaming/Channels/502/ -c:v copy -c:a copy -movflags +faststart+frag_keyframe+empty_moov -f matroska -" } ] } anacrolix-dms-669cd17/TODO000066400000000000000000000006431442657104700153250ustar00rootroot00000000000000* 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. anacrolix-dms-669cd17/data/000077500000000000000000000000001442657104700155435ustar00rootroot00000000000000anacrolix-dms-669cd17/data/VGC Sonic.png000066400000000000000000000547571442657104700177460ustar00rootroot00000000000000PNG  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`anacrolix-dms-669cd17/dlna/000077500000000000000000000000001442657104700155505ustar00rootroot00000000000000anacrolix-dms-669cd17/dlna/dlna.go000066400000000000000000000047161442657104700170250ustar00rootroot00000000000000package 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 // DLNA.ORG_FLAGS go here if you need to tweak. Flags string } 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))) // https://stackoverflow.com/questions/29182754/c-dlna-generate-dlna-org-flags // DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE | DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | DLNA_ORG_FLAG_CONNECTION_STALL | DLNA_ORG_FLAG_DLNA_V15 flags := "01700000000000000000000000000000" if cf.Flags != "" { flags = cf.Flags } params = append(params, "DLNA.ORG_FLAGS="+flags) 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 } anacrolix-dms-669cd17/dlna/dlna_test.go000066400000000000000000000004351442657104700200560ustar00rootroot00000000000000package 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) } } anacrolix-dms-669cd17/dlna/dms/000077500000000000000000000000001442657104700163335ustar00rootroot00000000000000anacrolix-dms-669cd17/dlna/dms/cd-service-desc.go000066400000000000000000000343711442657104700216320ustar00rootroot00000000000000package 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 ` anacrolix-dms-669cd17/dlna/dms/cds.go000066400000000000000000000360451442657104700174430ustar00rootroot00000000000000package dms import ( "encoding/json" "encoding/xml" "fmt" "io/ioutil" "net/http" "net/url" "os" "path" "path/filepath" "sort" "strconv" "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" ) const dmsMetadataSuffix = ".dms.json" type contentDirectoryService struct { *Server upnp.Eventing } func (cds *contentDirectoryService) updateIDString() string { return fmt.Sprintf("%d", uint32(os.Getpid())) } type dmsDynamicStreamResource struct { // (optional) DLNA profile name to include in the response e.g. MPEG_PS_PAL DlnaProfileName string // (optional) DLNA.ORG_FLAGS if you need to override the default (8D500000000000000000000000000000) DlnaFlags string // required: mime type, e.g. video/mpeg MimeType string // (optional) resolution, e.g. 640x360 Resolution string // (optional) bitrate, e.g. 721 Bitrate uint // required: OS command to generate this resource on the fly Command string } type dmsDynamicMediaItem struct { // (optional) Title of this media item. Defaults to the filename, if omitted Title string // (optional) duration, e.g. 0:21:37.922 Duration string // required: an array of available versions Resources []dmsDynamicStreamResource } func readDynamicStream(metadataPath string) (*dmsDynamicMediaItem, error) { bytes, err := ioutil.ReadFile(metadataPath) if err != nil { return nil, err } var re dmsDynamicMediaItem err = json.Unmarshal(bytes, &re) if err != nil { return nil, err } return &re, nil } func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host, userAgent string) (ret interface{}, err error) { // at this point we know that entryFilePath points to a .dms.json file; slurp and parse dmsMediaItem, err := readDynamicStream(cdsObject.FilePath()) if err != nil { me.Logger.Printf("%s ignored: %v", cdsObject.FilePath(), err) return } obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), } 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.videoItem" obj.Title = dmsMediaItem.Title if obj.Title == "" { obj.Title = strings.TrimSuffix(fileInfo.Name(), dmsMetadataSuffix) } item := upnpav.Item{ Object: obj, // Capacity: 1 for icon, plus resources. Res: make([]upnpav.Resource, 0, 1+len(dmsMediaItem.Resources)), } for i, dmsStream := range dmsMediaItem.Resources { // default flags borrowed from Serviio: DLNA_ORG_FLAG_SENDER_PACED | DLNA_ORG_FLAG_S0_INCREASE | DLNA_ORG_FLAG_SN_INCREASE | DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE | DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | DLNA_ORG_FLAG_DLNA_V15 flags := "8D500000000000000000000000000000" if dmsStream.DlnaFlags != "" { flags = dmsStream.DlnaFlags } item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, Path: resPath, RawQuery: url.Values{ "path": {cdsObject.Path}, "index": {strconv.Itoa(i)}, }.Encode(), }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", dmsStream.MimeType, dlna.ContentFeatures{ ProfileName: dmsStream.DlnaProfileName, SupportRange: false, SupportTimeSeek: false, Transcoded: true, Flags: flags, }.String()), Bitrate: dmsStream.Bitrate, Duration: dmsMediaItem.Duration, Resolution: dmsStream.Resolution, }) } // and an icon 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 } // 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 } isDmsMetadata := strings.HasSuffix(entryFilePath, dmsMetadataSuffix) if !fileInfo.IsDir() && me.AllowDynamicStreams && isDmsMetadata { return me.cdsObjectDynamicStreamToUpnpavObject(cdsObject, fileInfo, host, userAgent) } 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() { if isDmsMetadata { me.Logger.Levelf( log.Debug, "ignored %q: enable support for dynamic streams via the -allowDynamicStreams command line flag", cdsObject.FilePath()) } else { me.Logger.Levelf(log.Debug, "ignored %q: 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)...) } item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, Path: subtitlePath, RawQuery: url.Values{ "path": {cdsObject.Path}, }.Encode(), }).String(), ProtocolInfo: "http-get:*:text/plain", }) } 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] } anacrolix-dms-669cd17/dlna/dms/cds_test.go000066400000000000000000000007041442657104700204730ustar00rootroot00000000000000package 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() } } anacrolix-dms-669cd17/dlna/dms/cm-service-desc.go000066400000000000000000000126551442657104700216440ustar00rootroot00000000000000package 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 ` anacrolix-dms-669cd17/dlna/dms/cms.go000066400000000000000000000070241442657104700174470ustar00rootroot00000000000000package 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 } } anacrolix-dms-669cd17/dlna/dms/dms.go000077500000000000000000001002411442657104700174460ustar00rootroot00000000000000package dms import ( "bytes" "crypto/md5" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "math/rand" "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/ffprobe" ) // This is used when communicating with other devices, such as over HTTP. I don't imagine we're // popular enough to have special treatment yet. This value would change if we made potentially // breaking changes to our behaviour that other devices might want to act on. const serverVersion = "1" var ( serverField = fmt.Sprintf(`Linux/3.4 DLNADOC/1.50 UPnP/1.0 %s/%s`, userAgentProduct, serverVersion) rootDeviceModelName = fmt.Sprintf("%s %s", userAgentProduct, serverVersion) ) const ( userAgentProduct = "dms" rootDeviceType = "urn:schemas-upnp-org:device:MediaServer:1" resPath = "/res" iconPath = "/icon" subtitlePath = "/subtitle" rootDescPath = "/rootDesc.xml" contentDirectoryEventSubURL = "/evt/ContentDirectory" serviceControlURL = "/ctl" deviceIconPath = "/deviceIcon" ) type transcodeSpec struct { mimeType string DLNAProfileName string DLNAFlags 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 // Activate support for dynamic streams configured via .dms.json metadata files // This feature is not enabled by default, since having write access to a shared media // folder allows executing arbitrary commands in the context of the DLNA server. AllowDynamicStreams bool // pattern where to write transcode logs to. The [tsname] placeholder is replaced with the name // of the item currently being played. The default is $HOME/.dms/log/[tsname] TranscodeLogPattern string 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, dynamicMode bool) (r dlna.NPTRange, partialResponse, ok bool) { if dynamicMode || 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 writeResponseCode(w http.ResponseWriter, partialResponse bool) { w.WriteHeader(func() int { if partialResponse { return http.StatusPartialContent } else { return http.StatusOK } }()) } func (me *Server) serveDLNATranscode(w http.ResponseWriter, r *http.Request, path_ string, ts transcodeSpec, tsname string, dynamicMode bool) { w.Header().Set(dlna.TransferModeDomain, "Streaming") w.Header().Set("content-type", ts.mimeType) w.Header().Set(dlna.ContentFeaturesDomain, (dlna.ContentFeatures{ Transcoded: true, SupportTimeSeek: !dynamicMode, ProfileName: ts.DLNAProfileName, Flags: ts.DLNAFlags, }).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, dynamicMode) if !ok { return } // Samsung Frame TVs send a HEAD request first. If we don't terminate processing here, // the TV will keep reading the data and crash eventually :) if r.Method == "HEAD" { writeResponseCode(w, partialResponse) return } var logTsName string if !dynamicMode { 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) } } logTsName = filepath.Join(tsname, filepath.Base(path_)) } else { logTsName = tsname } stderrPath := strings.Replace(me.TranscodeLogPattern, "[tsname]", logTsName, -1) var logFile io.Writer if stderrPath != "" { os.MkdirAll(filepath.Dir(stderrPath), 0o750) aLogFile, err := os.Create(stderrPath) if err != nil { log.Printf("couldn't create transcode log file: %s", err) } else { defer aLogFile.Close() log.Printf("logging transcode to %q", stderrPath) } logFile = aLogFile } 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. writeResponseCode(w, partialResponse) 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) // Compatibility with Samsung Frame TV's - they don't display an empty content directory without this hack: bodyStr = strings.Replace(bodyStr, """, `"`, -1) 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" } args := []string{} _, fqThumbnail := os.LookupEnv("DMS_THUMBNAIL_FULLQUALITY") if fqThumbnail { args = append(args, "-s", "0", "-q", "10") } _, randThumbnail := os.LookupEnv("DMS_THUMBNAIL_RANDOM") if randThumbnail { args = append(args, "-t", strconv.Itoa(rand.Intn(100))) } args = append(args, "-i", filePath, "-o", "/dev/stdout", "-c"+c) cmd := exec.Command("ffmpegthumbnailer", args...) // 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 (me *Server) serveSubtitle(w http.ResponseWriter, r *http.Request) { filePath := me.filePath(r.URL.Query().Get("path")) subtitleFilePath := strings.TrimSuffix(filePath, filepath.Ext(filePath)) + ".srt" http.ServeFile(w, r, subtitleFilePath) } 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) serveDynamicStream(w http.ResponseWriter, r *http.Request, metadataPath string) error { dmsMediaItem, err := readDynamicStream(metadataPath) if err != nil { return err } aindex := 0 index := r.URL.Query().Get("index") if index != "" { aindex, err = strconv.Atoi(index) if err != nil { return err } } if aindex < 0 || aindex >= len(dmsMediaItem.Resources) { return fmt.Errorf("invalid index %d, corresponding stream not found", aindex) } dmsStream := dmsMediaItem.Resources[aindex] dmsTsSpec := transcodeSpec{ DLNAProfileName: dmsStream.DlnaProfileName, DLNAFlags: dmsStream.DlnaFlags, mimeType: dmsStream.MimeType, Transcode: transcode.Exec, } server.serveDLNATranscode(w, r, dmsStream.Command, dmsTsSpec, filepath.Base(metadataPath), true) return nil } 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(subtitlePath, server.serveSubtitle) 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 } if strings.HasSuffix(filePath, dmsMetadataSuffix) { if server.AllowDynamicStreams { err := server.serveDynamicStream(w, r, filePath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } else { http.Error(w, "dynamic streams are disabled", 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)) w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(path.Base(filePath))) 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, false) }) 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 } anacrolix-dms-669cd17/dlna/dms/dms_others.go000066400000000000000000000003551442657104700210340ustar00rootroot00000000000000//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) } anacrolix-dms-669cd17/dlna/dms/dms_test.go000066400000000000000000000032041442657104700205030ustar00rootroot00000000000000package 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()) } anacrolix-dms-669cd17/dlna/dms/dms_unix.go000066400000000000000000000006311442657104700205100ustar00rootroot00000000000000//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 } } anacrolix-dms-669cd17/dlna/dms/dms_unix_test.go000066400000000000000000000011061442657104700215450ustar00rootroot00000000000000//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) } } } anacrolix-dms-669cd17/dlna/dms/dms_windows.go000066400000000000000000000013371442657104700212230ustar00rootroot00000000000000//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) } anacrolix-dms-669cd17/dlna/dms/ffmpeg.go000066400000000000000000000007041442657104700201270ustar00rootroot00000000000000package 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 } anacrolix-dms-669cd17/dlna/dms/html.go000066400000000000000000000006021442657104700176240ustar00rootroot00000000000000package dms import ( "html/template" ) var rootTmpl *template.Template func init() { rootTmpl = template.Must(template.New("root").Parse( `
Path:
`)) } anacrolix-dms-669cd17/dlna/dms/mimetype.go000066400000000000000000000045471442657104700205250ustar00rootroot00000000000000package 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 } anacrolix-dms-669cd17/dlna/dms/mrrs-desc.go000066400000000000000000000047561442657104700205750ustar00rootroot00000000000000package 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 ` anacrolix-dms-669cd17/dlna/dms/mrrs.go000066400000000000000000000010431442657104700176430ustar00rootroot00000000000000package 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 } } anacrolix-dms-669cd17/go.mod000066400000000000000000000004421442657104700157400ustar00rootroot00000000000000module 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 ) anacrolix-dms-669cd17/go.sum000066400000000000000000000144521442657104700157730ustar00rootroot00000000000000github.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= anacrolix-dms-669cd17/helpers/000077500000000000000000000000001442657104700162745ustar00rootroot00000000000000anacrolix-dms-669cd17/helpers/bsd/000077500000000000000000000000001442657104700170445ustar00rootroot00000000000000anacrolix-dms-669cd17/helpers/bsd/dms000066400000000000000000000007461442657104700175610ustar00rootroot00000000000000#!/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" anacrolix-dms-669cd17/helpers/systemd/000077500000000000000000000000001442657104700177645ustar00rootroot00000000000000anacrolix-dms-669cd17/helpers/systemd/dms.service000066400000000000000000000004501442657104700221300ustar00rootroot00000000000000# 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 anacrolix-dms-669cd17/main.go000077500000000000000000000227571442657104700161250ustar00rootroot00000000000000package 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 AllowDynamicStreams bool TranscodeLogPattern string } 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'") transcodeLogPattern := flag.String("transcodeLogPattern", "", "pattern where to write transcode logs to. The [tsname] placeholder is replaced with the name of the item currently being played. The default is $HOME/.dms/log/[tsname]") 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.BoolVar(&config.AllowDynamicStreams, "allowDynamicStreams", false, "activate support for dynamic streams described via .dms.json metadata files") 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 config.TranscodeLogPattern = *transcodeLogPattern if config.TranscodeLogPattern == "" { u, err := user.Current() if err != nil { return fmt.Errorf("unable to resolve current user: %q", err) } config.TranscodeLogPattern = filepath.Join(u.HomeDir, ".dms", "log", "[tsname]") } 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, AllowDynamicStreams: config.AllowDynamicStreams, ForceTranscodeTo: config.ForceTranscodeTo, TranscodeLogPattern: config.TranscodeLogPattern, 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 } anacrolix-dms-669cd17/misc/000077500000000000000000000000001442657104700155655ustar00rootroot00000000000000anacrolix-dms-669cd17/misc/dms-win32/000077500000000000000000000000001442657104700173105ustar00rootroot00000000000000anacrolix-dms-669cd17/misc/dms-win32/NOTES000066400000000000000000000005751442657104700201320ustar00rootroot00000000000000This 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. anacrolix-dms-669cd17/misc/dms-win32/README.txt000066400000000000000000000003031442657104700210020ustar00rootroot00000000000000Double 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.anacrolix-dms-669cd17/misc/dms-win32/dms-gui.bat000066400000000000000000000000511442657104700213410ustar00rootroot00000000000000set PATH=%~dp0\bin start dms-gtk-gui.exeanacrolix-dms-669cd17/misc/misc.go000066400000000000000000000005241442657104700170500ustar00rootroot00000000000000package 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 } anacrolix-dms-669cd17/misc/misc_test.go000066400000000000000000000004231442657104700201050ustar00rootroot00000000000000package 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) } } anacrolix-dms-669cd17/play/000077500000000000000000000000001442657104700155775ustar00rootroot00000000000000anacrolix-dms-669cd17/play/attrs.go000066400000000000000000000005561442657104700172710ustar00rootroot00000000000000//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) } anacrolix-dms-669cd17/play/bool.go000066400000000000000000000001601442657104700170560ustar00rootroot00000000000000//go:build ignore // +build ignore package main import "fmt" func main() { fmt.Printf("%q%c\n", 3, false) } anacrolix-dms-669cd17/play/browse.xml000066400000000000000000000012361442657104700176240ustar00rootroot00000000000000 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 anacrolix-dms-669cd17/play/closure.go000066400000000000000000000005111442657104700175770ustar00rootroot00000000000000//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 } } anacrolix-dms-669cd17/play/execbug.go000066400000000000000000000005601442657104700175510ustar00rootroot00000000000000//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) } } anacrolix-dms-669cd17/play/execgood.go000066400000000000000000000007511442657104700177260ustar00rootroot00000000000000//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) } } anacrolix-dms-669cd17/play/ffprobe.go000066400000000000000000000004451442657104700175540ustar00rootroot00000000000000//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) } } anacrolix-dms-669cd17/play/getsortcaps.xml000066400000000000000000000004641442657104700206630ustar00rootroot00000000000000 anacrolix-dms-669cd17/play/mime.go000066400000000000000000000003361442657104700170570ustar00rootroot00000000000000//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)) } } anacrolix-dms-669cd17/play/parse_http_version.go000066400000000000000000000002601442657104700220420ustar00rootroot00000000000000//go:build ignore // +build ignore package main import ( "fmt" "net/http" "strings" ) func main() { fmt.Println(http.ParseHTTPVersion(strings.TrimSpace("HTTP/1.1 "))) } anacrolix-dms-669cd17/play/print-ifs.go000066400000000000000000000007741442657104700200510ustar00rootroot00000000000000//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) } } } anacrolix-dms-669cd17/play/scpd.go000066400000000000000000000014031442657104700170550ustar00rootroot00000000000000//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)) } anacrolix-dms-669cd17/play/soap.go000066400000000000000000000016311442657104700170710ustar00rootroot00000000000000//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)) } anacrolix-dms-669cd17/play/termsig/000077500000000000000000000000001442657104700172515ustar00rootroot00000000000000anacrolix-dms-669cd17/play/termsig/main.go000066400000000000000000000002401442657104700205200ustar00rootroot00000000000000package main import ( "fmt" "os" "os/signal" ) func main() { c := make(chan os.Signal, 0x100) signal.Notify(c) for i := range c { fmt.Println(i) } } anacrolix-dms-669cd17/play/transcode.go000066400000000000000000000012011442657104700201020ustar00rootroot00000000000000//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) } anacrolix-dms-669cd17/play/url.go000066400000000000000000000003611442657104700167300ustar00rootroot00000000000000//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_) } anacrolix-dms-669cd17/rrcache/000077500000000000000000000000001442657104700162415ustar00rootroot00000000000000anacrolix-dms-669cd17/rrcache/rrcache.go000066400000000000000000000030531442657104700202000ustar00rootroot00000000000000// 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 } anacrolix-dms-669cd17/soap/000077500000000000000000000000001442657104700155745ustar00rootroot00000000000000anacrolix-dms-669cd17/soap/soap.go000066400000000000000000000026521442657104700170720ustar00rootroot00000000000000package 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}, } } */ anacrolix-dms-669cd17/ssdp/000077500000000000000000000000001442657104700156035ustar00rootroot00000000000000anacrolix-dms-669cd17/ssdp/ssdp.go000066400000000000000000000167141442657104700171140ustar00rootroot00000000000000package 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() } anacrolix-dms-669cd17/transcode/000077500000000000000000000000001442657104700166145ustar00rootroot00000000000000anacrolix-dms-669cd17/transcode/transcode.go000066400000000000000000000131241442657104700211260ustar00rootroot00000000000000// Package transcode implements routines for transcoding to various kinds of // receiver. package transcode import ( "fmt" "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) } // credit laurent @ https://stackoverflow.com/questions/34118732/parse-a-command-line-string-into-flags-and-arguments-in-golang func parseCommandLine(command string) ([]string, error) { var args []string state := "start" current := "" quote := "\"" escapeNext := true for i := 0; i < len(command); i++ { c := command[i] if state == "quotes" { if string(c) != quote { current += string(c) } else { args = append(args, current) current = "" state = "start" } continue } if escapeNext { current += string(c) escapeNext = false continue } if c == '\\' { escapeNext = true continue } if c == '"' || c == '\'' { state = "quotes" quote = string(c) continue } if state == "arg" { if c == ' ' || c == '\t' { args = append(args, current) current = "" state = "start" } else { current += string(c) } continue } if c != ' ' && c != '\t' { state = "arg" current += string(c) } } if state == "quotes" { return []string{}, fmt.Errorf("Unclosed quote in command line: %s", command) } if current != "" { args = append(args, current) } return args, nil } // Exec runs the cmd to generate the video to stream. It does not support seeking. Used by the dynamic stream feature. func Exec(cmds string, start, length time.Duration, stderr io.Writer) (r io.ReadCloser, err error) { cmda, aerr := parseCommandLine(cmds) if aerr != nil { err = aerr return } return transcodePipe(cmda, stderr) } anacrolix-dms-669cd17/upnp/000077500000000000000000000000001442657104700156145ustar00rootroot00000000000000anacrolix-dms-669cd17/upnp/eventing.go000066400000000000000000000044771442657104700177760ustar00rootroot00000000000000package 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 } anacrolix-dms-669cd17/upnp/eventing_test.go000066400000000000000000000014041442657104700210200ustar00rootroot00000000000000package 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)) } } anacrolix-dms-669cd17/upnp/upnp.go000066400000000000000000000100311442657104700171200ustar00rootroot00000000000000package 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]) } anacrolix-dms-669cd17/upnpav/000077500000000000000000000000001442657104700161435ustar00rootroot00000000000000anacrolix-dms-669cd17/upnpav/upnpav.go000066400000000000000000000034201442657104700200020ustar00rootroot00000000000000package 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) }